From aef6dd4707b19babd28bb93164bdd61d8029a89a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Nov 2019 09:11:08 -0500 Subject: [PATCH 01/52] Upgrade handlebars to 4.5.3 (#51486) Co-authored-by: Elastic Machine --- package.json | 2 +- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a1873134e3cb8..a4f7b869aef6f 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "globby": "^8.0.1", "good-squeeze": "2.1.0", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", diff --git a/x-pack/package.json b/x-pack/package.json index 84ce92bf8e9e6..f84db22fe5c40 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -244,7 +244,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", diff --git a/yarn.lock b/yarn.lock index 64d33426d7aa4..3296fc013c48d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14310,10 +14310,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.3.5.tgz#d6c2d0a0f08b4479e3949f8321c0f3893bb691be" - integrity sha512-I16T/l8X9DV3sEkY9sK9lsPRgDsj82ayBY/4pAZyP2BcX5WeRM3O06bw9kIs2GLrHvFB/DNzWWJyFvof8wQGqw== +handlebars@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" From a1b01f4a7582dd35f236393672130871a0e03f56 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 25 Nov 2019 15:14:55 +0100 Subject: [PATCH 02/52] - Update _template completion to work even when the index does not exist (#51592) - Update _template body completions --- .../components/template_autocomplete_component.js | 2 +- .../spec/overrides/indices.put_template.json | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js index e6cae3f1710bc..0c00b2f93ee6f 100644 --- a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js +++ b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js @@ -21,7 +21,7 @@ import { ListComponent } from './list_component'; export class TemplateAutocompleteComponent extends ListComponent { constructor(name, parent) { - super(name, mappings.getTemplates, parent); + super(name, mappings.getTemplates, parent, true, true); } getContextKey() { return 'template'; diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json index cc7218be2e48d..c19836e2f9eb0 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json @@ -1,10 +1,16 @@ { "indices.put_template": { "data_autocomplete_rules": { - "template": "index*", - "warmers": { "__scope_link": "_warmer" }, + "index_patterns": [], "mappings": { "__scope_link": "put_mapping" }, - "settings": { "__scope_link": "put_settings" } + "settings": { "__scope_link": "put_settings" }, + "version": 0, + "order": 0, + "aliases": { + "__template": { + "NAME": {} + } + } }, "patterns": [ "_template/{template}" From ce2dd0dc669af17ae84016ef374884d5be28ef1f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 25 Nov 2019 15:18:34 +0100 Subject: [PATCH 03/52] Add cumulative cardinality per https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-cardinality-aggregation.html (#51591) --- .../console/server/api_server/es_6_0/aggregations.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js index e54bc2476698a..c2596dc4258da 100644 --- a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js +++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js @@ -352,6 +352,13 @@ const rules = { }, missing: '', }, + cumulative_cardinality: { + __template: { + buckets_path: '', + }, + buckets_path: '', + format: '', + }, scripted_metric: { __template: { init_script: '', From 6f01aa962e3f7fe6c57d3d0ac7337c595e4e3fc9 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 25 Nov 2019 08:49:36 -0600 Subject: [PATCH 04/52] EUI i18n token updates (#51307) * updated i18n tokens for eui * i18n snapshot * translation file removals * fix typo --- .../__snapshots__/i18n_service.test.tsx.snap | 56 +++- src/core/public/i18n/i18n_service.tsx | 271 +++++++++++++++++- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 4 files changed, 314 insertions(+), 21 deletions(-) diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index d159c588718fe..d0374511515d1 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -10,6 +10,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiBasicTable.selectThisRow": "Select this row", "euiBasicTable.tableDescription": [Function], "euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.", + "euiBreadcrumbs.collapsedBadge.ariaLabel": "Show all breadcrumbs", "euiCardSelect.select": "Select", "euiCardSelect.selected": "Selected", "euiCardSelect.unavailable": "Unavailable", @@ -21,6 +22,20 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiCollapsedItemActions.allActions": "All actions", "euiColorPicker.screenReaderAnnouncement": "A popup with a range of selectable colors opened. Tab forward to cycle through colors choices or press escape to close this popup.", "euiColorPicker.swatchAriaLabel": [Function], + "euiColorStopThumb.removeLabel": "Remove this stop", + "euiColorStopThumb.screenReaderAnnouncement": "A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.", + "euiColorStops.screenReaderAnnouncement": [Function], + "euiColumnSelector.hideAll": "Hide all", + "euiColumnSelector.selectAll": "Show all", + "euiColumnSorting.clearAll": "Clear sorting", + "euiColumnSorting.emptySorting": "Currently no fields are sorted", + "euiColumnSorting.pickFields": "Pick fields to sort by", + "euiColumnSorting.sortFieldAriaLabel": "Sort by:", + "euiColumnSortingDraggable.activeSortLabel": "is sorting this data grid", + "euiColumnSortingDraggable.defaultSortAsc": "A-Z", + "euiColumnSortingDraggable.defaultSortDesc": "Z-A", + "euiColumnSortingDraggable.removeSortLabel": "Remove from data grid sort:", + "euiColumnSortingDraggable.toggleLegend": "Select sorting method for field:", "euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options", "euiComboBoxOptionsList.alreadyAdded": [Function], "euiComboBoxOptionsList.createCustomOption": [Function], @@ -28,6 +43,19 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available", "euiComboBoxOptionsList.noMatchingOptions": [Function], "euiComboBoxPill.removeSelection": [Function], + "euiCommonlyUsedTimeRanges.legend": "Commonly used", + "euiDataGrid.screenReaderNotice": "Cell contains interactive content.", + "euiDataGridCell.expandButtonTitle": "Click or hit enter to interact with cell content", + "euiDataGridSchema.booleanSortTextAsc": "True-False", + "euiDataGridSchema.booleanSortTextDesc": "False-True", + "euiDataGridSchema.currencySortTextAsc": "Low-High", + "euiDataGridSchema.currencySortTextDesc": "High-Low", + "euiDataGridSchema.dateSortTextAsc": "New-Old", + "euiDataGridSchema.dateSortTextDesc": "Old-New", + "euiDataGridSchema.jsonSortTextAsc": "Small-Large", + "euiDataGridSchema.jsonSortTextDesc": "Large-Small", + "euiDataGridSchema.numberSortTextAsc": "Low-High", + "euiDataGridSchema.numberSortTextDesc": "High-Low", "euiFilterButton.filterBadge": [Function], "euiForm.addressFormErrors": "Please address the errors in your form.", "euiFormControlLayoutClearButton.label": "Clear input", @@ -35,25 +63,45 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiHeaderLinks.appNavigation": "App navigation", "euiHeaderLinks.openNavigationMenu": "Open navigation menu", "euiHue.label": "Select the HSV color mode \\"hue\\" value", + "euiImage.closeImage": [Function], + "euiImage.openImage": [Function], + "euiLink.external.ariaLabel": "External link", "euiModal.closeModal": "Closes this modal window", "euiPagination.jumpToLastPage": [Function], "euiPagination.nextPage": "Next page", "euiPagination.pageOfTotal": [Function], "euiPagination.previousPage": "Previous page", - "euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.", + "euiPopover.screenReaderAnnouncement": "You are in a dialog. To close this dialog, hit escape.", + "euiQuickSelect.applyButton": "Apply", + "euiQuickSelect.fullDescription": [Function], + "euiQuickSelect.legendText": "Quick select a time range", + "euiQuickSelect.nextLabel": "Next time window", + "euiQuickSelect.previousLabel": "Previous time window", + "euiQuickSelect.quickSelectTitle": "Quick select", + "euiQuickSelect.tenseLabel": "Time tense", + "euiQuickSelect.unitLabel": "Time unit", + "euiQuickSelect.valueLabel": "Time value", + "euiRefreshInterval.fullDescription": [Function], + "euiRefreshInterval.legend": "Refresh every", + "euiRefreshInterval.start": "Start", + "euiRefreshInterval.stop": "Stop", + "euiRelativeTab.fullDescription": [Function], + "euiRelativeTab.relativeDate": [Function], + "euiRelativeTab.roundingLabel": [Function], + "euiRelativeTab.unitInputLabel": "Relative time span", "euiSaturation.roleDescription": "HSV color mode saturation and value selection", "euiSaturation.screenReaderAnnouncement": "Use the arrow keys to navigate the square color gradient. The coordinates resulting from each key press will be used to calculate HSV color mode \\"saturation\\" and \\"value\\" numbers, in the range of 0 to 1. Left and right decrease and increase (respectively) the \\"saturation\\" value. Up and down decrease and increase (respectively) the \\"value\\" value.", "euiSelectable.loadingOptions": "Loading options", "euiSelectable.noAvailableOptions": "There aren't any options available", "euiSelectable.noMatchingOptions": [Function], "euiStat.loadingText": "Statistic is loading", - "euiStep.completeStep": "Step", - "euiStep.incompleteStep": "Incomplete Step", + "euiStep.ariaLabel": [Function], "euiStepHorizontal.buttonTitle": [Function], "euiStepHorizontal.step": "Step", "euiStepNumber.hasErrors": "has errors", "euiStepNumber.hasWarnings": "has warnings", "euiStepNumber.isComplete": "complete", + "euiStyleSelector.buttonText": "Density", "euiSuperDatePicker.showDatesButtonLabel": "Show dates", "euiSuperSelect.screenReaderAnnouncement": [Function], "euiSuperSelectControl.selectAnOption": [Function], @@ -68,6 +116,8 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiToast.dismissToast": "Dismiss toast", "euiToast.newNotification": "A new notification appears", "euiToast.notification": "Notification", + "euiTreeView.ariaLabel": [Function], + "euiTreeView.listNavigationInstructions": "You can quickly navigate this list using arrow keys.", }, } } diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 17cdf56cd43c0..721c5d49634f4 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -65,6 +65,13 @@ export class I18nService { 'Screen reader announcement that functionality is available in the page document', } ), + 'euiBreadcrumbs.collapsedBadge.ariaLabel': i18n.translate( + 'core.euiBreadcrumbs.collapsedBadge.ariaLabel', + { + defaultMessage: 'Show all breadcrumbs', + description: 'Displayed when one or more breadcrumbs are hidden.', + } + ), 'euiCardSelect.select': i18n.translate('core.euiCardSelect.select', { defaultMessage: 'Select', description: 'Displayed button text when a card option can be selected.', @@ -117,6 +124,80 @@ export class I18nService { description: 'Screen reader text to describe the action and hex value of the selectable option', }), + 'euiColorStopThumb.removeLabel': i18n.translate('core.euiColorStopThumb.removeLabel', { + defaultMessage: 'Remove this stop', + description: 'Label accompanying a button whose action will remove the color stop', + }), + 'euiColorStopThumb.screenReaderAnnouncement': i18n.translate( + 'core.euiColorStopThumb.screenReaderAnnouncement', + { + defaultMessage: + 'A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.', + description: + 'Message when the color picker popover has opened for an individual color stop thumb.', + } + ), + 'euiColorStops.screenReaderAnnouncement': ({ label, readOnly, disabled }: EuiValues) => + i18n.translate('core.euiColorStops.screenReaderAnnouncement', { + defaultMessage: + '{label}: {readOnly} {disabled} Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop.', + values: { label, readOnly, disabled }, + description: + 'Screen reader text to describe the composite behavior of the color stops component.', + }), + 'euiColumnSelector.hideAll': i18n.translate('core.euiColumnSelector.hideAll', { + defaultMessage: 'Hide all', + }), + 'euiColumnSelector.selectAll': i18n.translate('core.euiColumnSelector.selectAll', { + defaultMessage: 'Show all', + }), + 'euiColumnSorting.clearAll': i18n.translate('core.euiColumnSorting.clearAll', { + defaultMessage: 'Clear sorting', + }), + 'euiColumnSorting.emptySorting': i18n.translate('core.euiColumnSorting.emptySorting', { + defaultMessage: 'Currently no fields are sorted', + }), + 'euiColumnSorting.pickFields': i18n.translate('core.euiColumnSorting.pickFields', { + defaultMessage: 'Pick fields to sort by', + }), + 'euiColumnSorting.sortFieldAriaLabel': i18n.translate( + 'core.euiColumnSorting.sortFieldAriaLabel', + { + defaultMessage: 'Sort by:', + } + ), + 'euiColumnSortingDraggable.activeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.activeSortLabel', + { + defaultMessage: 'is sorting this data grid', + } + ), + 'euiColumnSortingDraggable.defaultSortAsc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortAsc', + { + defaultMessage: 'A-Z', + description: 'Ascending sort label', + } + ), + 'euiColumnSortingDraggable.defaultSortDesc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortDesc', + { + defaultMessage: 'Z-A', + description: 'Descending sort label', + } + ), + 'euiColumnSortingDraggable.removeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.removeSortLabel', + { + defaultMessage: 'Remove from data grid sort:', + } + ), + 'euiColumnSortingDraggable.toggleLegend': i18n.translate( + 'core.euiColumnSortingDraggable.toggleLegend', + { + defaultMessage: 'Select sorting method for field:', + } + ), 'euiComboBoxOptionsList.allOptionsSelected': i18n.translate( 'core.euiComboBoxOptionsList.allOptionsSelected', { @@ -163,6 +244,88 @@ export class I18nService { values: { children }, description: 'ARIA label, `children` is the human-friendly value of an option', }), + 'euiCommonlyUsedTimeRanges.legend': i18n.translate('core.euiCommonlyUsedTimeRanges.legend', { + defaultMessage: 'Commonly used', + }), + 'euiDataGrid.screenReaderNotice': i18n.translate('core.euiDataGrid.screenReaderNotice', { + defaultMessage: 'Cell contains interactive content.', + }), + 'euiDataGridCell.expandButtonTitle': i18n.translate( + 'core.euiDataGridCell.expandButtonTitle', + { + defaultMessage: 'Click or hit enter to interact with cell content', + } + ), + 'euiDataGridSchema.booleanSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextAsc', + { + defaultMessage: 'True-False', + description: 'Ascending boolean label', + } + ), + 'euiDataGridSchema.booleanSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextDesc', + { + defaultMessage: 'False-True', + description: 'Descending boolean label', + } + ), + 'euiDataGridSchema.currencySortTextAsc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending currency label', + } + ), + 'euiDataGridSchema.currencySortTextDesc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending currency label', + } + ), + 'euiDataGridSchema.dateSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextAsc', + { + defaultMessage: 'New-Old', + description: 'Ascending date label', + } + ), + 'euiDataGridSchema.dateSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextDesc', + { + defaultMessage: 'Old-New', + description: 'Descending date label', + } + ), + 'euiDataGridSchema.numberSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending number label', + } + ), + 'euiDataGridSchema.numberSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending number label', + } + ), + 'euiDataGridSchema.jsonSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextAsc', + { + defaultMessage: 'Small-Large', + description: 'Ascending size label', + } + ), + 'euiDataGridSchema.jsonSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextDesc', + { + defaultMessage: 'Large-Small', + description: 'Descending size label', + } + ), 'euiFilterButton.filterBadge': ({ count, hasActiveFilters }: EuiValues) => i18n.translate('core.euiFilterButton.filterBadge', { defaultMessage: '${count} ${filterCountLabel} filters', @@ -195,6 +358,19 @@ export class I18nService { 'euiHue.label': i18n.translate('core.euiHue.label', { defaultMessage: 'Select the HSV color mode "hue" value', }), + 'euiImage.closeImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.closeImage', { + defaultMessage: 'Close full screen {alt} image', + values: { alt }, + }), + 'euiImage.openImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.openImage', { + defaultMessage: 'Open full screen {alt} image', + values: { alt }, + }), + 'euiLink.external.ariaLabel': i18n.translate('core.euiLink.external.ariaLabel', { + defaultMessage: 'External link', + }), 'euiModal.closeModal': i18n.translate('core.euiModal.closeModal', { defaultMessage: 'Closes this modal window', }), @@ -217,9 +393,70 @@ export class I18nService { 'euiPopover.screenReaderAnnouncement': i18n.translate( 'core.euiPopover.screenReaderAnnouncement', { - defaultMessage: 'You are in a popup. To exit this popup, hit Escape.', + defaultMessage: 'You are in a dialog. To close this dialog, hit escape.', } ), + 'euiQuickSelect.applyButton': i18n.translate('core.euiQuickSelect.applyButton', { + defaultMessage: 'Apply', + }), + 'euiQuickSelect.fullDescription': ({ timeTense, timeValue, timeUnit }: EuiValues) => + i18n.translate('core.euiQuickSelect.fullDescription', { + defaultMessage: 'Currently set to {timeTense} {timeValue} {timeUnit}.', + values: { timeTense, timeValue, timeUnit }, + }), + 'euiQuickSelect.legendText': i18n.translate('core.euiQuickSelect.legendText', { + defaultMessage: 'Quick select a time range', + }), + 'euiQuickSelect.nextLabel': i18n.translate('core.euiQuickSelect.nextLabel', { + defaultMessage: 'Next time window', + }), + 'euiQuickSelect.previousLabel': i18n.translate('core.euiQuickSelect.previousLabel', { + defaultMessage: 'Previous time window', + }), + 'euiQuickSelect.quickSelectTitle': i18n.translate('core.euiQuickSelect.quickSelectTitle', { + defaultMessage: 'Quick select', + }), + 'euiQuickSelect.tenseLabel': i18n.translate('core.euiQuickSelect.tenseLabel', { + defaultMessage: 'Time tense', + }), + 'euiQuickSelect.unitLabel': i18n.translate('core.euiQuickSelect.unitLabel', { + defaultMessage: 'Time unit', + }), + 'euiQuickSelect.valueLabel': i18n.translate('core.euiQuickSelect.valueLabel', { + defaultMessage: 'Time value', + }), + 'euiRefreshInterval.fullDescription': ({ optionValue, optionText }: EuiValues) => + i18n.translate('core.euiRefreshInterval.fullDescription', { + defaultMessage: 'Currently set to {optionValue} {optionText}.', + values: { optionValue, optionText }, + }), + 'euiRefreshInterval.legend': i18n.translate('core.euiRefreshInterval.legend', { + defaultMessage: 'Refresh every', + }), + 'euiRefreshInterval.start': i18n.translate('core.euiRefreshInterval.start', { + defaultMessage: 'Start', + }), + 'euiRefreshInterval.stop': i18n.translate('core.euiRefreshInterval.stop', { + defaultMessage: 'Stop', + }), + 'euiRelativeTab.fullDescription': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.fullDescription', { + defaultMessage: 'The unit is changeable. Currently set to {unit}.', + values: { unit }, + }), + 'euiRelativeTab.relativeDate': ({ position }: EuiValues) => + i18n.translate('core.euiRelativeTab.relativeDate', { + defaultMessage: '{position} date', + values: { position }, + }), + 'euiRelativeTab.roundingLabel': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.roundingLabel', { + defaultMessage: 'Round to the {unit}', + values: { unit }, + }), + 'euiRelativeTab.unitInputLabel': i18n.translate('core.euiRelativeTab.unitInputLabel', { + defaultMessage: 'Relative time span', + }), 'euiSaturation.roleDescription': i18n.translate('core.euiSaturation.roleDescription', { defaultMessage: 'HSV color mode saturation and value selection', }), @@ -247,22 +484,18 @@ export class I18nService { 'euiStat.loadingText': i18n.translate('core.euiStat.loadingText', { defaultMessage: 'Statistic is loading', }), - 'euiStep.completeStep': i18n.translate('core.euiStep.completeStep', { - defaultMessage: 'Step', - description: - 'See https://elastic.github.io/eui/#/navigation/steps to know how Step control looks like', - }), - 'euiStep.incompleteStep': i18n.translate('core.euiStep.incompleteStep', { - defaultMessage: 'Incomplete Step', - }), + 'euiStep.ariaLabel': ({ status }: EuiValues) => + i18n.translate('core.euiStep.ariaLabel', { + defaultMessage: '{stepStatus}', + values: { stepStatus: status === 'incomplete' ? 'Incomplete Step' : 'Step' }, + }), 'euiStepHorizontal.buttonTitle': ({ step, title, disabled, isComplete }: EuiValues) => { return i18n.translate('core.euiStepHorizontal.buttonTitle', { - defaultMessage: - 'Step {step}: {title}{titleAppendix, select, completed { is completed} disabled { is disabled} other {}}', + defaultMessage: 'Step {step}: {title}{titleAppendix}', values: { step, title, - titleAppendix: disabled ? 'disabled' : isComplete ? 'completed' : '', + titleAppendix: disabled ? ' is disabled' : isComplete ? ' is complete' : '', }, }); }, @@ -285,6 +518,9 @@ export class I18nService { description: 'Used as the title attribute on an image or svg icon to indicate a given process step is complete', }), + 'euiStyleSelector.buttonText': i18n.translate('core.euiStyleSelector.buttonText', { + defaultMessage: 'Density', + }), 'euiSuperDatePicker.showDatesButtonLabel': i18n.translate( 'core.euiSuperDatePicker.showDatesButtonLabel', { @@ -362,6 +598,17 @@ export class I18nService { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), + 'euiTreeView.ariaLabel': ({ nodeLabel, ariaLabel }: EuiValues) => + i18n.translate('core.euiTreeView.ariaLabel', { + defaultMessage: '{nodeLabel} child of {ariaLabel}', + values: { nodeLabel, ariaLabel }, + }), + 'euiTreeView.listNavigationInstructions': i18n.translate( + 'core.euiTreeView.listNavigationInstructions', + { + defaultMessage: 'You can quickly navigate this list using arrow keys.', + } + ), }; return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f35a650cdf1b6..f83d0c9ea3c9a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -616,8 +616,6 @@ "core.euiSelectable.noAvailableOptions": "利用可能なオプションがありません", "core.euiSelectable.noMatchingOptions": "{searchValue} はどのオプションにも一致していません", "core.euiStat.loadingText": "統計を読み込み中です", - "core.euiStep.completeStep": "手順", - "core.euiStep.incompleteStep": "未完了の手順", "core.euiStepHorizontal.buttonTitle": "ステップ {step}: {title}{titleAppendix, select, completed { が完了} 無効 { が無効} other {}}", "core.euiStepHorizontal.step": "手順", "core.euiStepNumber.hasErrors": "エラーがあります", @@ -12759,4 +12757,4 @@ "xpack.licensing.check.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", "xpack.licensing.check.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fb5b7cc191e61..a830eaacd29e3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -617,8 +617,6 @@ "core.euiSelectable.noAvailableOptions": "没有任何可用选项", "core.euiSelectable.noMatchingOptions": "{searchValue} 不匹配任何选项", "core.euiStat.loadingText": "统计正在加载", - "core.euiStep.completeStep": "步骤", - "core.euiStep.incompleteStep": "未完成步骤", "core.euiStepHorizontal.buttonTitle": "第 {step} 步:{title}{titleAppendix, select, completed {已完成} disabled {已禁用} other {}}", "core.euiStepHorizontal.step": "步骤", "core.euiStepNumber.hasErrors": "有错误", @@ -12848,4 +12846,4 @@ "xpack.licensing.check.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", "xpack.licensing.check.errorUnsupportedMessage": "您的{licenseType}许可证不支持 {pluginName}。请升级您的许可证。" } -} \ No newline at end of file +} From 132f6b1e0900e22293d99409769cef4ff800a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 25 Nov 2019 10:24:09 -0500 Subject: [PATCH 05/52] Rename alertTypeParams to params (#51255) --- x-pack/legacy/plugins/alerting/README.md | 4 +- x-pack/legacy/plugins/alerting/mappings.json | 2 +- .../alerting/server/alerts_client.test.ts | 298 +++++++++--------- .../plugins/alerting/server/alerts_client.ts | 10 +- .../server/lib/task_runner_factory.test.ts | 4 +- .../server/lib/task_runner_factory.ts | 4 +- .../lib/validate_alert_type_params.test.ts | 2 +- .../server/lib/validate_alert_type_params.ts | 2 +- .../alerting/server/routes/create.test.ts | 20 +- .../plugins/alerting/server/routes/create.ts | 4 +- .../alerting/server/routes/get.test.ts | 2 +- .../alerting/server/routes/update.test.ts | 10 +- .../plugins/alerting/server/routes/update.ts | 4 +- .../legacy/plugins/alerting/server/types.ts | 4 +- .../detection_engine/alerts/create_signals.ts | 2 +- .../alerts/read_signals.test.ts | 32 +- .../detection_engine/alerts/read_signals.ts | 2 +- .../lib/detection_engine/alerts/types.ts | 2 +- .../detection_engine/alerts/update_signals.ts | 8 +- .../routes/__mocks__/request_responses.ts | 2 +- .../lib/detection_engine/routes/utils.test.ts | 10 +- .../lib/detection_engine/routes/utils.ts | 42 +-- .../common/lib/alert_utils.ts | 2 +- .../common/lib/get_test_alert_data.ts | 2 +- .../tests/alerting/alerts.ts | 10 +- .../tests/alerting/create.ts | 10 +- .../tests/alerting/find.ts | 4 +- .../security_and_spaces/tests/alerting/get.ts | 2 +- .../tests/alerting/update.ts | 16 +- .../spaces_only/tests/alerting/alerts.ts | 6 +- .../spaces_only/tests/alerting/create.ts | 2 +- .../spaces_only/tests/alerting/find.ts | 2 +- .../spaces_only/tests/alerting/get.ts | 2 +- .../spaces_only/tests/alerting/update.ts | 4 +- .../es_archives/hybrid/kibana/mappings.json | 4 +- .../es_archives/lens/basic/mappings.json | 4 +- .../es_archives/lens/reporting/mappings.json | 4 +- .../es_archives/ml/farequote/mappings.json | 2 +- .../es_archives/reporting/nanos/mappings.json | 4 +- 39 files changed, 275 insertions(+), 275 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 456eb6732c81c..40f61d11e9ace 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -202,7 +202,7 @@ Payload: |tags|A list of keywords to reference and search in the future.|string[]| |alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `DELETE /api/alert/{id}`: Delete alert @@ -246,7 +246,7 @@ Payload: |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `POST /api/alert/{id}/_enable`: Enable an alert diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index 7a1be777aff44..f840c019d5e02 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -31,7 +31,7 @@ } } }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index dc3aaaf5cf23c..08607f04a5235 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -48,7 +48,7 @@ function getMockData(overwrites: Record = {}) { alertTypeId: '123', interval: '10s', throttle: null, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -80,7 +80,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -130,25 +130,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); @@ -164,9 +164,6 @@ describe('create()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": undefined, "apiKeyOwner": undefined, "createdBy": "elastic", @@ -175,6 +172,9 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -240,7 +240,7 @@ describe('create()', () => { enabled: false, alertTypeId: '123', interval: 10000, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -263,30 +263,30 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": false, - "id": "1", - "interval": 10000, - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "enabled": false, + "id": "1", + "interval": 10000, + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); - test('should validate alertTypeParams', async () => { + test('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); const data = getMockData(); alertTypeRegistry.get.mockReturnValueOnce({ @@ -302,7 +302,7 @@ describe('create()', () => { async executor() {}, }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); @@ -337,7 +337,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -387,7 +387,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -448,7 +448,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -511,7 +511,7 @@ describe('create()', () => { ], alertTypeId: '123', name: 'abc', - alertTypeParams: { bar: true }, + params: { bar: true }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', createdBy: 'elastic', @@ -923,7 +923,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -946,24 +946,24 @@ describe('get()', () => { }); const result = await alertsClient.get({ id: '1' }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -981,7 +981,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1016,7 +1016,7 @@ describe('find()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1041,31 +1041,31 @@ describe('find()', () => { }); const result = await alertsClient.find(); expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1086,7 +1086,7 @@ describe('delete()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, scheduledTaskId: 'task-123', @@ -1155,7 +1155,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1183,7 +1183,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1198,25 +1198,25 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1233,14 +1233,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": null, "apiKeyOwner": null, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1291,7 +1291,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1320,7 +1320,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1335,26 +1335,26 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": "MTIzOmFiYw==", - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1371,14 +1371,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1400,7 +1400,7 @@ describe('update()', () => { `); }); - it('should validate alertTypeParams', async () => { + it('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); alertTypeRegistry.get.mockReturnValueOnce({ id: '123', @@ -1428,7 +1428,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1443,7 +1443,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); }); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index c260a754e4594..3916ec1d62b6c 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -77,7 +77,7 @@ interface UpdateOptions { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; }; } @@ -111,7 +111,7 @@ export class AlertsClient { public async create({ data, options }: CreateOptions) { // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const apiKey = await this.createAPIKey(); const username = await this.getUserName(); @@ -125,7 +125,7 @@ export class AlertsClient { apiKey: apiKey.created ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') : undefined, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, muteAll: false, mutedInstanceIds: [], }); @@ -199,7 +199,7 @@ export class AlertsClient { const apiKey = await this.createAPIKey(); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); this.validateActions(alertType, data.actions); const { actions, references } = this.extractReferences(data.actions); @@ -210,7 +210,7 @@ export class AlertsClient { { ...attributes, ...data, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, actions, updatedBy: username, apiKeyOwner: apiKey.created ? username : null, diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts index dcc74ed9488ce..1d91d4a35d588 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts @@ -76,7 +76,7 @@ const mockedAlertTypeSavedObject = { alertTypeId: '123', interval: '10s', mutedInstanceIds: [], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -253,7 +253,7 @@ test('validates params before executing the alert type', async () => { references: [], }); await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts index 66d445f57fe73..051b15fc8dd8f 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts @@ -94,12 +94,12 @@ export class TaskRunnerFactory { const services = getServices(fakeRequest); // Ensure API key is still valid and user has access const { - attributes: { alertTypeParams, actions, interval, throttle, muteAll, mutedInstanceIds }, + attributes: { params, actions, interval, throttle, muteAll, mutedInstanceIds }, references, } = await services.savedObjectsClient.get('alert', alertId); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, params); // Inject ids into actions const actionsWithIds = actions.map(action => { diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts index f33746798769b..e9a61354001f1 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts @@ -61,6 +61,6 @@ test('should validate and throw error when params is invalid', () => { {} ) ).toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts index 6070f2d99b605..248d896c06ac2 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -19,6 +19,6 @@ export function validateAlertTypeParams>( try { return validator.validate(params); } catch (err) { - throw Boom.badRequest(`alertTypeParams invalid: ${err.message}`); + throw Boom.badRequest(`params invalid: ${err.message}`); } } diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index c67d1a7b32352..318dbdf068d6a 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -15,7 +15,7 @@ const mockedAlert = { name: 'abc', interval: '10s', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -57,12 +57,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "id": "123", "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -83,12 +83,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -112,12 +112,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index 65fbae7c8b298..fb82a03f172b3 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -17,7 +17,7 @@ interface ScheduleRequest extends Hapi.Request { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -41,7 +41,7 @@ export const createAlertRoute = { alertTypeId: Joi.string().required(), throttle: getDurationSchema().default(null), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts index 84938a0e927d1..19618bc9e39fe 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts @@ -14,7 +14,7 @@ const mockedAlert = { id: '1', alertTypeId: '1', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index ee98f7d6dd9d3..7fc3f45911010 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -17,7 +17,7 @@ const mockedResponse = { alertTypeId: '1', tags: ['foo'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -40,7 +40,7 @@ test('calls the update function with proper parameters', async () => { name: 'abc', tags: ['bar'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -74,11 +74,11 @@ test('calls the update function with proper parameters', async () => { }, }, ], - "alertTypeParams": Object { - "otherField": false, - }, "interval": "12s", "name": "abc", + "params": Object { + "otherField": false, + }, "tags": Array [ "bar", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 9c8e0296c2f78..6aeedb93a1098 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -19,7 +19,7 @@ interface UpdateRequest extends Hapi.Request { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -43,7 +43,7 @@ export const updateAlertRoute = { .items(Joi.string()) .required(), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 359b88e21cc3b..e2460c549c05d 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -65,7 +65,7 @@ export interface Alert { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; @@ -83,7 +83,7 @@ export interface RawAlert extends SavedObjectAttributes { alertTypeId: string; interval: string; actions: RawAlertAction[]; - alertTypeParams: SavedObjectAttributes; + params: SavedObjectAttributes; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 9f472d060def7..420f995431423 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -40,7 +40,7 @@ export const createSignals = async ({ name, tags: [], alertTypeId: SIGNALS_ID, - alertTypeParams: { + params: { description, ruleId, index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts index dde3f19b1c66d..39d1fac8f7a09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts @@ -129,11 +129,11 @@ describe('read_signals', () => { test('should return a single value of rule-1 with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -150,11 +150,11 @@ describe('read_signals', () => { test('should return a single value of rule-2 with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -171,11 +171,11 @@ describe('read_signals', () => { test('should return null for a made up value with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -194,8 +194,8 @@ describe('read_signals', () => { test('returns null if the objects are not of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: 'made up 1', alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + { alertTypeId: 'made up 1', params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); @@ -205,30 +205,30 @@ describe('read_signals', () => { test('returns correct type if the objects are of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '123' } }); + expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '456' } }); + expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts index f73074b560cb2..3c49112aaf50b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts @@ -14,7 +14,7 @@ export const findSignalInArrayByRuleId = ( if (isAlertTypeArray(objects)) { const signals: SignalAlertType[] = objects; const signal: SignalAlertType[] = signals.filter(datum => { - return datum.alertTypeParams.ruleId === ruleId; + return datum.params.ruleId === ruleId; }); if (signal.length !== 0) { return signal[0]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 79e62538b1a7e..9c6e1f99c672b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -137,7 +137,7 @@ export type AlertTypeParams = Omit ({ name: 'Detect Root/Admin Users', tags: [], alertTypeId: 'siem.signals', - alertTypeParams: { + params: { description: 'Detecting root and admin users', ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 3d7f0a9fd049a..22dd7be5fbba7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -75,7 +75,7 @@ describe('utils', () => { test('should omit query if query is null', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = null; + fullSignal.params.query = null; const signal = transformAlertToSignal(fullSignal); expect(signal).toEqual({ created_by: 'elastic', @@ -105,7 +105,7 @@ describe('utils', () => { test('should omit query if query is undefined', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; + fullSignal.params.query = undefined; const signal = transformAlertToSignal(fullSignal); expect(signal).toEqual({ created_by: 'elastic', @@ -135,8 +135,8 @@ describe('utils', () => { test('should omit a mix of undefined, null, and missing fields', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; - fullSignal.alertTypeParams.language = null; + fullSignal.params.query = undefined; + fullSignal.params.language = null; const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); expect(omitData).toEqual({ created_by: 'elastic', @@ -194,7 +194,7 @@ describe('utils', () => { test('should return immutable is equal to false', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.immutable = false; + fullSignal.params.immutable = false; const signalWithEnabledFalse = transformAlertToSignal(fullSignal); expect(signalWithEnabledFalse).toEqual({ created_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index bf39d9d16b2b9..e3a677741efca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -29,32 +29,32 @@ export const getIdError = ({ export const transformAlertToSignal = (signal: SignalAlertType): Partial => { return pickBy((value: unknown) => value != null, { created_by: signal.createdBy, - description: signal.alertTypeParams.description, + description: signal.params.description, enabled: signal.enabled, - false_positives: signal.alertTypeParams.falsePositives, - filter: signal.alertTypeParams.filter, - filters: signal.alertTypeParams.filters, - from: signal.alertTypeParams.from, + false_positives: signal.params.falsePositives, + filter: signal.params.filter, + filters: signal.params.filters, + from: signal.params.from, id: signal.id, - immutable: signal.alertTypeParams.immutable, - index: signal.alertTypeParams.index, + immutable: signal.params.immutable, + index: signal.params.index, interval: signal.interval, - rule_id: signal.alertTypeParams.ruleId, - language: signal.alertTypeParams.language, - output_index: signal.alertTypeParams.outputIndex, - max_signals: signal.alertTypeParams.maxSignals, - risk_score: signal.alertTypeParams.riskScore, + rule_id: signal.params.ruleId, + language: signal.params.language, + output_index: signal.params.outputIndex, + max_signals: signal.params.maxSignals, + risk_score: signal.params.riskScore, name: signal.name, - query: signal.alertTypeParams.query, - references: signal.alertTypeParams.references, - saved_id: signal.alertTypeParams.savedId, - meta: signal.alertTypeParams.meta, - severity: signal.alertTypeParams.severity, - size: signal.alertTypeParams.size, + query: signal.params.query, + references: signal.params.references, + saved_id: signal.params.savedId, + meta: signal.params.meta, + severity: signal.params.severity, + size: signal.params.size, updated_by: signal.updatedBy, - tags: signal.alertTypeParams.tags, - to: signal.alertTypeParams.to, - type: signal.alertTypeParams.type, + tags: signal.params.tags, + to: signal.params.to, + type: signal.params.type, }); }; diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 4fbb13b229003..57b4b3b6c26c6 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -183,7 +183,7 @@ export class AlertUtils { throttle: '1m', tags: [], alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts index d7fba7e43c372..ae382652b6234 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts @@ -13,7 +13,7 @@ export function getTestAlertData(overwrites = {}) { interval: '1m', throttle: '1m', actions: [], - alertTypeParams: {}, + params: {}, ...overwrites, }; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index c43e159bbe8ca..09a642d1d14bb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -166,7 +166,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -258,7 +258,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -356,7 +356,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, @@ -491,7 +491,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', 'other'], @@ -560,7 +560,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', null, 'default'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index d94556d6cedda..bf61ee2e3f137 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -59,7 +59,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: user.username, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, @@ -173,10 +173,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['name', 'alertTypeId', 'interval', 'alertTypeParams', 'actions'], + keys: ['name', 'alertTypeId', 'interval', 'params', 'actions'], }, }); break; @@ -185,7 +185,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); - it(`should handle create alert request appropriately when alertTypeParams isn't valid`, async () => { + it(`should handle create alert request appropriately when params isn't valid`, async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') @@ -214,7 +214,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index b04c0f44e7dd4..31af7a0acffbb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -62,7 +62,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', @@ -119,7 +119,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index cfb2f34ca8056..1a8109f6b6b3c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -56,7 +56,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: response.body.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 78f70ddb13edd..1b1bcef9ad23f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -33,7 +33,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -93,7 +93,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -142,7 +142,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], throttle: '1m', alertTypeId: '1', - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -203,10 +203,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['throttle', 'name', 'tags', 'interval', 'alertTypeParams', 'actions'], + keys: ['throttle', 'name', 'tags', 'interval', 'params', 'actions'], }, }); break; @@ -222,7 +222,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.validation', - alertTypeParams: { + params: { param1: 'test', }, }) @@ -239,7 +239,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], interval: '1m', throttle: '1m', - alertTypeParams: {}, + params: {}, actions: [], }); @@ -261,7 +261,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 28634c46b6350..9af4848c57d7d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -125,7 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { getTestAlertData({ interval: '1m', alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -193,7 +193,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -238,7 +238,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 80459690af732..3018f8efffffe 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -41,7 +41,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: null, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index f49d774fc1e92..0d12af6db79b2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -45,7 +45,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: match.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index ef27a2713e98a..9e4797bcbf7ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -39,7 +39,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index 942eff0766722..a6eccf88d9e26 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -28,7 +28,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -68,7 +68,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['foo'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', diff --git a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json index 18b359d37aaa6..5256e29956f4f 100644 --- a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json @@ -99,7 +99,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1068,4 +1068,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/basic/mappings.json b/x-pack/test/functional/es_archives/lens/basic/mappings.json index b87dbe12a7005..f2a29f022ff5e 100644 --- a/x-pack/test/functional/es_archives/lens/basic/mappings.json +++ b/x-pack/test/functional/es_archives/lens/basic/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1291,4 +1291,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/reporting/mappings.json b/x-pack/test/functional/es_archives/lens/reporting/mappings.json index 0321d57bc2df6..8b8e5a0e6e7f6 100644 --- a/x-pack/test/functional/es_archives/lens/reporting/mappings.json +++ b/x-pack/test/functional/es_archives/lens/reporting/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1300,4 +1300,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/ml/farequote/mappings.json b/x-pack/test/functional/es_archives/ml/farequote/mappings.json index 4fe559cc85fe1..b00545c015a74 100644 --- a/x-pack/test/functional/es_archives/ml/farequote/mappings.json +++ b/x-pack/test/functional/es_archives/ml/farequote/mappings.json @@ -133,7 +133,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json index 34420b6bb63e1..dd717387a2643 100644 --- a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json @@ -84,7 +84,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1091,4 +1091,4 @@ } } } -} \ No newline at end of file +} From 1ce02049aa3b52f7ebfd2516c965b8aebd327c4d Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 25 Nov 2019 09:38:49 -0600 Subject: [PATCH 06/52] Swap renovate codeowners with assignee configuration (#48987) * Swap renovate codeowners with assignee configuration * remove : --- .github/CODEOWNERS | 1 - renovate.json5 | 3 +++ src/dev/renovate/config.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd73e60d1c914..4e2abd5a3db1c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,7 +45,6 @@ /x-pack/test/functional/services/transform.ts @elastic/ml-ui # Operations -/renovate.json5 @elastic/kibana-operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations diff --git a/renovate.json5 b/renovate.json5 index aefbc61e8dc12..3886715618e99 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -21,6 +21,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -28,6 +29,7 @@ major: { labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -228,6 +230,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts index 7e62059f5059a..6acbbaa4d5255 100644 --- a/src/dev/renovate/config.ts +++ b/src/dev/renovate/config.ts @@ -21,7 +21,7 @@ import { RENOVATE_PACKAGE_GROUPS } from './package_groups'; import { PACKAGE_GLOBS } from './package_globs'; import { wordRegExp, maybeFlatMap, maybeMap, getTypePackageName } from './utils'; -const DEFAULT_LABELS = ['release_note:skip', 'renovate', 'v8.0.0', 'v7.6.0']; +const DEFAULT_LABELS = ['release_note:skip', 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0']; export const RENOVATE_CONFIG = { extends: ['config:base'], From 0607032ba5b6df3341d98cf66043c3ac71d88ea7 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 25 Nov 2019 17:56:49 +0200 Subject: [PATCH 07/52] =?UTF-8?q?Move=20FilterBar=20component=20=E2=87=92?= =?UTF-8?q?=20NP=20(#51178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Moved filter bar * Fixed import * Fixed search bar test * change css import path * fix imports from data/public --- src/core/MIGRATION.md | 4 +- .../apply_filter_popover_content.tsx | 8 +++- .../public/filter/filter_bar/filter_bar.less | 0 .../core_plugins/data/public/filter/index.tsx | 2 - .../core_plugins/data/public/index.scss | 2 +- src/legacy/core_plugins/data/public/index.ts | 2 +- .../search_bar/components/search_bar.test.tsx | 7 ++- .../search_bar/components/search_bar.tsx | 3 +- src/plugins/data/public/index.ts | 2 + .../ui}/filter_bar/_global_filter_group.scss | 0 .../ui}/filter_bar/_global_filter_item.scss | 0 .../data/public/ui}/filter_bar/_index.scss | 0 .../public/ui}/filter_bar/_variables.scss | 0 .../data/public/ui}/filter_bar/filter_bar.tsx | 4 +- .../filter_editor/_filter_editor.scss | 0 .../ui}/filter_bar/filter_editor/_index.scss | 0 .../filter_editor/generic_combo_box.tsx | 0 .../ui}/filter_bar/filter_editor/index.tsx | 7 +-- .../__snapshots__/filter_label.test.js.snap | 0 .../lib/filter_editor_utils.test.ts | 43 +++++++------------ .../filter_editor/lib/filter_editor_utils.ts | 9 +--- .../filter_editor/lib/filter_label.test.js | 5 +-- .../filter_editor/lib/filter_label.tsx | 2 +- .../filter_editor/lib/filter_operators.ts | 2 +- .../filter_editor/phrase_suggestor.tsx | 11 +---- .../filter_editor/phrase_value_input.tsx | 2 +- .../filter_editor/phrases_values_input.tsx | 2 +- .../filter_editor/range_value_input.tsx | 4 +- .../filter_editor/value_input_type.tsx | 0 .../public/ui}/filter_bar/filter_item.tsx | 2 +- .../public/ui}/filter_bar/filter_options.tsx | 0 .../ui}/filter_bar/filter_view/index.tsx | 2 +- .../data/public/ui/filter_bar/index.ts | 21 +++++++++ .../data/public/ui}/index.ts | 2 +- .../legacy/plugins/ml/common/types/fields.ts | 2 +- .../transform/public/app/common/pivot_aggs.ts | 2 +- .../public/app/common/pivot_group_by.ts | 2 +- 37 files changed, 77 insertions(+), 77 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_global_filter_group.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_global_filter_item.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_index.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_variables.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_bar.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/_filter_editor.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/_index.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/generic_combo_box.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/index.tsx (99%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_editor_utils.test.ts (79%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_editor_utils.ts (93%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_label.test.js (88%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_label.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_operators.ts (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrase_suggestor.tsx (91%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrase_value_input.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrases_values_input.tsx (96%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/range_value_input.tsx (96%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/value_input_type.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_item.tsx (98%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_options.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_view/index.tsx (97%) create mode 100644 src/plugins/data/public/ui/filter_bar/index.ts rename src/{legacy/core_plugins/data/public/filter/filter_bar => plugins/data/public/ui}/index.ts (95%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f3532cd717ac8..22c96110742e0 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1167,8 +1167,8 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | -| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | +| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx index 37d96a51d66d2..954cbca8f054b 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx @@ -31,8 +31,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { IndexPattern } from '../../index_patterns'; -import { FilterLabel } from '../filter_bar/filter_editor/lib/filter_label'; -import { mapAndFlattenFilters, esFilters, utils } from '../../../../../../plugins/data/public'; +import { + mapAndFlattenFilters, + esFilters, + utils, + FilterLabel, +} from '../../../../../../plugins/data/public'; interface Props { filters: esFilters.Filter[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx index 005c4904a4f39..e48a18fc53a76 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/index.tsx @@ -17,6 +17,4 @@ * under the License. */ -export { FilterBar } from './filter_bar'; - export { ApplyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss index 14274d27c13ee..913141666c7b9 100644 --- a/src/legacy/core_plugins/data/public/index.scss +++ b/src/legacy/core_plugins/data/public/index.scss @@ -2,6 +2,6 @@ @import './query/query_bar/index'; -@import './filter/filter_bar/index'; +@import 'src/plugins/data/public/ui/filter_bar/index'; @import './search/search_bar/index'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index ffce162cadde4..b33aef75e6756 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,7 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { FilterBar, ApplyFiltersPopover } from './filter'; +export { ApplyFiltersPopover } from './filter'; export { Field, FieldType, diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 44637365247fb..0ca9482fefa30 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -35,9 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('../../../../../data/public', () => { +jest.mock('../../../../../../../plugins/data/public', () => { return { FilterBar: () =>
, + }; +}); + +jest.mock('../../../../../data/public', () => { + return { QueryBarInput: () =>
, }; }); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index c8b76c9cda99d..6a1ef77a56653 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { IndexPattern, FilterBar } from '../../../../../data/public'; +import { IndexPattern } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; import { SavedQueryMeta, SaveQueryForm } from './saved_query_management/save_query_form'; @@ -41,6 +41,7 @@ import { Query, esFilters, TimeHistoryContract, + FilterBar, } from '../../../../../../../plugins/data/public'; interface SearchBarInjectedDeps { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 6a2df6a61d136..ace0b44378b45 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -36,3 +36,5 @@ export * from './types'; export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; export * from './query'; + +export * from './ui'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_group.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_item.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss b/src/plugins/data/public/ui/filter_bar/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss rename to src/plugins/data/public/ui/filter_bar/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss rename to src/plugins/data/public/ui/filter_bar/_variables.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx rename to src/plugins/data/public/ui/filter_bar/filter_bar.tsx index e80bffb5e3c68..2f1b1f8588eb9 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -25,8 +25,8 @@ import React, { useState } from 'react'; import { FilterEditor } from './filter_editor'; import { FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; -import { IIndexPattern, esFilters } from '../../../../../../plugins/data/public'; +import { useKibana } from '../../../../kibana_react/public'; +import { IIndexPattern, esFilters } from '../..'; interface Props { filters: esFilters.Filter[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx similarity index 99% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 4f9424f30f516..12da4cbab02da 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -48,12 +48,7 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; -import { - esFilters, - utils, - IIndexPattern, - IFieldType, -} from '../../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern, IFieldType } from '../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts similarity index 79% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index 6dc9bc2300e04..2cc7f16cfe261 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -17,7 +17,6 @@ * under the License. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { existsFilter, phraseFilter, @@ -25,8 +24,8 @@ import { rangeFilter, stubIndexPattern, stubFields, -} from '../../../../../../../../plugins/data/public/stubs'; -import { IndexPattern, Field } from '../../../../index'; +} from '../../../../stubs'; +import { esFilters } from '../../../../index'; import { getFieldFromFilter, getFilterableFields, @@ -37,17 +36,12 @@ import { import { existsOperator, isBetweenOperator, isOneOfOperator, isOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; - jest.mock('ui/new_platform'); -const mockedFields = stubFields as Field[]; -const mockedIndexPattern = stubIndexPattern as IndexPattern; - describe('Filter editor utils', () => { describe('getFieldFromFilter', () => { it('should return the field from the filter', () => { - const field = getFieldFromFilter(phraseFilter, mockedIndexPattern); + const field = getFieldFromFilter(phraseFilter, stubIndexPattern); expect(field).not.toBeUndefined(); expect(field && field.name).toBe(phraseFilter.meta.key); }); @@ -117,12 +111,12 @@ describe('Filter editor utils', () => { describe('getFilterableFields', () => { it('returns the list of fields from the given index pattern', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); + const fieldOptions = getFilterableFields(stubIndexPattern); expect(fieldOptions.length).toBeGreaterThan(0); }); it('limits the fields to the filterable fields', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); + const fieldOptions = getFilterableFields(stubIndexPattern); const nonFilterableFields = fieldOptions.filter(field => !field.filterable); expect(nonFilterableFields.length).toBe(0); }); @@ -131,14 +125,14 @@ describe('Filter editor utils', () => { describe('getOperatorOptions', () => { it('returns range for number fields', () => { const [field] = stubFields.filter(({ type }) => type === 'number'); - const operatorOptions = getOperatorOptions(field as Field); + const operatorOptions = getOperatorOptions(field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).not.toBeUndefined(); }); it('does not return range for string fields', () => { const [field] = stubFields.filter(({ type }) => type === 'string'); - const operatorOptions = getOperatorOptions(field as Field); + const operatorOptions = getOperatorOptions(field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).toBeUndefined(); }); @@ -146,49 +140,44 @@ describe('Filter editor utils', () => { describe('isFilterValid', () => { it('should return false if index pattern is not provided', () => { - const isValid = isFilterValid(undefined, mockedFields[0], isOperator, 'foo'); + const isValid = isFilterValid(undefined, stubFields[0], isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if field is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, undefined, isOperator, 'foo'); + const isValid = isFilterValid(stubIndexPattern, undefined, isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if operator is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], undefined, 'foo'); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], undefined, 'foo'); expect(isValid).toBe(false); }); it('should return false for phrases filter without phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, []); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, []); expect(isValid).toBe(false); }); it('should return true for phrases filter with phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, ['foo']); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, ['foo']); expect(isValid).toBe(true); }); it('should return false for range filter without range', () => { - const isValid = isFilterValid( - mockedIndexPattern, - mockedFields[0], - isBetweenOperator, - undefined - ); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, undefined); expect(isValid).toBe(false); }); it('should return true for range filter with from', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { from: 'foo', }); expect(isValid).toBe(true); }); it('should return true for range filter with from/to', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { from: 'foo', too: 'goo', }); @@ -196,7 +185,7 @@ describe('Filter editor utils', () => { }); it('should return true for exists filter without params', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], existsOperator); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], existsOperator); expect(isValid).toBe(true); }); }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts index e4487af42beaf..422ffb162125d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -18,14 +18,9 @@ */ import dateMath from '@elastic/datemath'; -import { Ipv4Address } from '../../../../../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../../kibana_utils/public'; import { FILTER_OPERATORS, Operator } from './filter_operators'; -import { - esFilters, - IIndexPattern, - IFieldType, - isFilterable, -} from '../../../../../../../../plugins/data/public'; +import { esFilters, IIndexPattern, IFieldType, isFilterable } from '../../../..'; export function getFieldFromFilter(filter: esFilters.FieldFilter, indexPattern: IIndexPattern) { return indexPattern.fields.find(field => field.name === filter.meta.key); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js similarity index 88% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js index 0f45a33a79ebb..3eb46645522e1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js @@ -20,10 +20,7 @@ import React from 'react'; import { FilterLabel } from './filter_label'; import { shallow } from 'enzyme'; - -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import { phraseFilter } from '../../../../../../../../plugins/data/public/stubs'; -/* eslint-enable @kbn/eslint/no-restricted-paths */ +import { phraseFilter } from '../../../../stubs'; test('alias', () => { const filter = { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 1b4bdb881116b..49a0d6f2ab3bd 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -21,7 +21,7 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { existsOperator, isOneOfOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts index a3da03db71d6e..bb15cffa67b59 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; export interface Operator { message: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx similarity index 91% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 092bf8daa8f2e..61290cc16b8a8 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -19,16 +19,9 @@ import { Component } from 'react'; import { debounce } from 'lodash'; -import { - withKibana, - KibanaReactContextValue, -} from '../../../../../../../plugins/kibana_react/public'; -import { - IDataPluginServices, - IIndexPattern, - IFieldType, -} from '../../../../../../../plugins/data/public'; +import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; +import { IDataPluginServices, IIndexPattern, IFieldType } from '../../..'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx index 7ef51f88ba57e..b16994cb0057b 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx @@ -24,7 +24,7 @@ import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { value?: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx index f3b30e2ad5fd9..aa76684239b63 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx @@ -23,7 +23,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { values?: string[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx index 3c39a770377a0..65b842f0bd4aa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx @@ -21,8 +21,8 @@ import { EuiIcon, EuiLink, EuiFormHelpText, EuiFormControlLayoutDelimited } from import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React from 'react'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; -import { IFieldType } from '../../../../../../../plugins/data/public'; +import { useKibana } from '../../../../../kibana_react/public'; +import { IFieldType } from '../../..'; import { ValueInputType } from './value_input_type'; interface RangeParams { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx rename to src/plugins/data/public/ui/filter_bar/filter_item.tsx index 27406232dd5d3..4ef0b2740e5fa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import { UiSettingsClientContract } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; -import { esFilters, utils, IIndexPattern } from '../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern } from '../..'; interface Props { id: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx rename to src/plugins/data/public/ui/filter_bar/filter_options.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx rename to src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 39d4a80cdf540..dd12789d15a9d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -21,7 +21,7 @@ import { EuiBadge, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { FilterLabel } from '../filter_editor/lib/filter_label'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/plugins/data/public/ui/filter_bar/index.ts b/src/plugins/data/public/ui/filter_bar/index.ts new file mode 100644 index 0000000000000..b975317d46630 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FilterBar } from './filter_bar'; +export { FilterLabel } from './filter_editor/lib/filter_label'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts b/src/plugins/data/public/ui/index.ts similarity index 95% rename from src/legacy/core_plugins/data/public/filter/filter_bar/index.ts rename to src/plugins/data/public/ui/index.ts index 438d292b9f583..d0aaf2f6aac1c 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { FilterBar } from './filter_bar'; +export * from './filter_bar'; diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts index f9d9b6b0161e2..9e1b992eec907 100644 --- a/x-pack/legacy/plugins/ml/common/types/fields.ts +++ b/x-pack/legacy/plugins/ml/common/types/fields.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts index adc4bfd1b5918..af55732691bb0 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts index 85daad6c7fd52..e6792958ab5d2 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; From c7f8086e114d6d8bd39ab9b05f77e5c123a647cf Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 25 Nov 2019 16:02:53 +0000 Subject: [PATCH 08/52] Removes redundant code for awaiting migrations in Task Manager Thanks to #43433 being merged we no longer need to wait for the migrations to run as they are guaranteed to have run by the time plugin init has completed. This is just a cleanup making it easier to move towards the migration to the Kibana Platform. --- x-pack/legacy/plugins/task_manager/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/task_manager/index.ts b/x-pack/legacy/plugins/task_manager/index.ts index 0fda1490de714..0487b003bc1ef 100644 --- a/x-pack/legacy/plugins/task_manager/index.ts +++ b/x-pack/legacy/plugins/task_manager/index.ts @@ -72,16 +72,7 @@ export function taskManager(kibana: any) { } ); this.kbnServer.afterPluginsInit(() => { - (async () => { - // The code block below can't await directly within "afterPluginsInit" - // callback due to circular dependency. The server isn't "ready" until - // this code block finishes. Migrations wait for server to be ready before - // executing. Saved objects repository waits for migrations to finish before - // finishing the request. To avoid this, we'll await within a separate - // function block. - await this.kbnServer.server.kibanaMigrator.runMigrations(); - plugin.start(); - })(); + plugin.start(); }); server.expose(setupContract); }, From 2acc287fc2aca3f2b3552935bb988cf7899bc519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 25 Nov 2019 17:05:51 +0100 Subject: [PATCH 09/52] [Logs UI] log rate setup index validation (#50008) * Scaffold API endpoint * Implement the API endpoint * Implement API client * Set error messages in `useAnalysisSetupState` * Show validation errors next to the submit button * Check for setup errors regarding the selected indexes * Call validation only once Enrich the `availableIndices` array with validation information to show it later in the form. * Ensure validation runs before showing the indices * Adjust naming conventions - Replace `index_pattern` with `indices`, since it means something different in kibana. - Group validation actions under the `validation` namespace. * Move index error messages to the `InitialConfigurationStep` * Move error messages to the UI layer * Move validation call to `useAnalysisSetupState` * Pass timestamp as a parameter of `useAnalysisSetupState` * Fix regression with the index names in the API response * Use `_field_caps` api * s/timestamp/timestampField/g * Tweak error messages * Move `ValidationIndicesUIError` to `log_analysis_setup_state` * Track validation status It's safer to rely on the state of the promise instead of treating an empty array as "loading" * Handle network errors * Use individual `` elements for the indices This allows to disable individual checkboxes * Pass the whole `validatedIndices` array to the inner objects This will make easier to determine which indeces have errors in the checkbox list itself and simplify the state we keep track of. * Disable indices with errors Show a tooltip above the disabled index to explain why it cannot be selected. * Pass indices to the API as an array * Show overlay while the validation loads * Wrap tooltips on a `block` element Prevents the checkboxes from collapsing on the same line * Use the right dependencies for `useEffect => validateIndices()` * Restore formatter function name * Simplify mapping of selected indices to errors * s/checked/isSelected/g * Make errors field-generic * Allow multiple errors per index * Simplify code a bit --- .../common/http_api/log_analysis/index.ts | 1 + .../http_api/log_analysis/validation/index.ts | 7 + .../log_analysis/validation/indices.ts | 51 ++++++ x-pack/legacy/plugins/infra/package.json | 2 +- .../components/loading_overlay_wrapper.tsx | 1 + .../api/index_patterns_validate.ts | 33 ++++ .../logs/log_analysis/log_analysis_jobs.tsx | 5 +- .../log_analysis/log_analysis_setup_state.tsx | 107 ++++++++++--- .../pages/logs/analysis/page_content.tsx | 2 + .../logs/analysis/page_setup_content.tsx | 3 + .../analysis_setup_indices_form.tsx | 148 ++++++++++++------ .../initial_configuration_step.tsx | 92 ++++++++--- .../setup/process_step/process_step.tsx | 4 +- .../pages/logs/analysis/setup/setup_steps.tsx | 13 +- .../plugins/infra/server/infra_server.ts | 6 +- .../lib/adapters/framework/adapter_types.ts | 25 ++- .../infra/server/routes/log_analysis/index.ts | 1 + .../log_analysis/index_patterns/index.ts | 7 + .../log_analysis/index_patterns/validate.ts | 83 ++++++++++ 19 files changed, 487 insertions(+), 104 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts index 38684cb22e237..378e32cb3582c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './validation'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts new file mode 100644 index 0000000000000..727faca69298e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './indices'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts new file mode 100644 index 0000000000000..62d81dc136853 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices'; + +/** + * Request types + */ +export const validationIndicesRequestPayloadRT = rt.type({ + data: rt.type({ + timestampField: rt.string, + indices: rt.array(rt.string), + }), +}); + +export type ValidationIndicesRequestPayload = rt.TypeOf; + +/** + * Response types + * */ +export const validationIndicesErrorRT = rt.union([ + rt.type({ + error: rt.literal('INDEX_NOT_FOUND'), + index: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_FOUND'), + index: rt.string, + field: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_VALID'), + index: rt.string, + field: rt.string, + }), +]); + +export type ValidationIndicesError = rt.TypeOf; + +export const validationIndicesResponsePayloadRT = rt.type({ + data: rt.type({ + errors: rt.array(validationIndicesErrorRT), + }), +}); + +export type ValidationIndicesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/package.json b/x-pack/legacy/plugins/infra/package.json index 63812bb2da513..7aa8cb9b5269a 100644 --- a/x-pack/legacy/plugins/infra/package.json +++ b/x-pack/legacy/plugins/infra/package.json @@ -16,4 +16,4 @@ "boom": "7.3.0", "lodash": "^4.17.15" } -} \ No newline at end of file +} diff --git a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx index a99b265fc3ea9..5df1fc07e83b9 100644 --- a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx +++ b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx @@ -40,4 +40,5 @@ const OverlayDiv = euiStyled.div` position: absolute; top: 0; width: 100%; + z-index: ${props => props.theme.eui.euiZLevel1}; `; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts new file mode 100644 index 0000000000000..440ee10e4223d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { kfetch } from 'ui/kfetch'; + +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, +} from '../../../../../common/http_api'; + +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; + +export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => { + const response = await kfetch({ + method: 'POST', + pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + body: JSON.stringify( + validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } }) + ), + }); + + return pipe( + validationIndicesResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx index 163f0e39d1228..0f386f416b866 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -94,8 +94,8 @@ export const useLogAnalysisJobs = ({ dispatch({ type: 'fetchingJobStatuses' }); return await callJobsSummaryAPI(spaceId, sourceId); }, - onResolve: response => { - dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId }); + onResolve: jobResponse => { + dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId }); }, onReject: err => { dispatch({ type: 'failedFetchingJobStatuses' }); @@ -158,6 +158,7 @@ export const useLogAnalysisJobs = ({ setup: setupMlModule, setupMlModuleRequest, setupStatus: statusState.setupStatus, + timestampField: timeField, viewSetupForReconfiguration, viewSetupForUpdate, viewResults, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 7942657018455..c965c50bedccc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { isExampleDataIndex } from '../../../../common/log_analysis'; +import { + ValidationIndicesError, + ValidationIndicesResponsePayload, +} from '../../../../common/http_api'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callIndexPatternsValidate } from './api/index_patterns_validate'; type SetupHandler = ( indices: string[], @@ -14,42 +20,75 @@ type SetupHandler = ( endTime: number | undefined ) => void; +export type ValidationIndicesUIError = + | ValidationIndicesError + | { error: 'NETWORK_ERROR' } + | { error: 'TOO_FEW_SELECTED_INDICES' }; + +export interface ValidatedIndex { + index: string; + errors: ValidationIndicesError[]; + isSelected: boolean; +} + interface AnalysisSetupStateArguments { availableIndices: string[]; cleanupAndSetupModule: SetupHandler; setupModule: SetupHandler; + timestampField: string; } -type IndicesSelection = Record; - -type ValidationErrors = 'TOO_FEW_SELECTED_INDICES'; - const fourWeeksInMs = 86400000 * 7 * 4; export const useAnalysisSetupState = ({ availableIndices, cleanupAndSetupModule, setupModule, + timestampField, }: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); - const [selectedIndices, setSelectedIndices] = useState( - availableIndices.reduce( - (indexMap, indexName) => ({ - ...indexMap, - [indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)), - }), - {} - ) + // Prepare the validation + const [validatedIndices, setValidatedIndices] = useState( + availableIndices.map(index => ({ + index, + errors: [], + isSelected: false, + })) + ); + const [validateIndicesRequest, validateIndices] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callIndexPatternsValidate(timestampField, availableIndices); + }, + onResolve: ({ data }: ValidationIndicesResponsePayload) => { + setValidatedIndices( + availableIndices.map(index => { + const errors = data.errors.filter(error => error.index === index); + return { + index, + errors, + isSelected: errors.length === 0 && !isExampleDataIndex(index), + }; + }) + ); + }, + onReject: () => { + setValidatedIndices([]); + }, + }, + [availableIndices, timestampField] ); + useEffect(() => { + validateIndices(); + }, [validateIndices]); + const selectedIndexNames = useMemo( - () => - Object.entries(selectedIndices) - .filter(([_indexName, isSelected]) => isSelected) - .map(([indexName]) => indexName), - [selectedIndices] + () => validatedIndices.filter(i => i.isSelected).map(i => i.index), + [validatedIndices] ); const setup = useCallback(() => { @@ -60,24 +99,42 @@ export const useAnalysisSetupState = ({ return cleanupAndSetupModule(selectedIndexNames, startTime, endTime); }, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]); - const validationErrors: ValidationErrors[] = useMemo( + const isValidating = useMemo( () => - Object.values(selectedIndices).some(isSelected => isSelected) - ? [] - : ['TOO_FEW_SELECTED_INDICES' as const], - [selectedIndices] + validateIndicesRequest.state === 'pending' || + validateIndicesRequest.state === 'uninitialized', + [validateIndicesRequest.state] ); + const validationErrors = useMemo(() => { + if (isValidating) { + return []; + } + + if (validateIndicesRequest.state === 'rejected') { + return [{ error: 'NETWORK_ERROR' }]; + } + + if (selectedIndexNames.length === 0) { + return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; + } + + return validatedIndices.reduce((errors, index) => { + return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors; + }, []); + }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + return { cleanupAndSetup, endTime, + isValidating, selectedIndexNames, - selectedIndices, setEndTime, - setSelectedIndices, setStartTime, setup, startTime, + validatedIndices, + setValidatedIndices, validationErrors, }; }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx index 04d7520c0ca88..f0a26eae25ecb 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx @@ -27,6 +27,7 @@ export const AnalysisPageContent = () => { lastSetupErrorMessages, setup, setupStatus, + timestampField, viewResults, } = useContext(LogAnalysisJobs.Context); @@ -61,6 +62,7 @@ export const AnalysisPageContent = () => { errorMessages={lastSetupErrorMessages} setup={setup} setupStatus={setupStatus} + timestampField={timestampField} viewResults={viewResults} /> ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx index 097cccf5dca33..7ae174c4a7899 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx @@ -34,6 +34,7 @@ interface AnalysisSetupContentProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -43,6 +44,7 @@ export const AnalysisSetupContent: React.FunctionComponent { useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); @@ -82,6 +84,7 @@ export const AnalysisSetupContent: React.FunctionComponent diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx index defcefd69a7ab..585a65b9ad1c8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -4,37 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCheckboxGroup, EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; - -export type IndicesSelection = Record; - -export type IndicesValidationError = 'TOO_FEW_SELECTED_INDICES'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; +import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ - indices: IndicesSelection; - onChangeSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; -}> = ({ indices, onChangeSelectedIndices, validationErrors = [] }) => { + indices: ValidatedIndex[]; + isValidating: boolean; + onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void; + valid: boolean; +}> = ({ indices, isValidating, onChangeSelectedIndices, valid }) => { + const handleCheckboxChange = useCallback( + (event: React.ChangeEvent) => { + onChangeSelectedIndices( + indices.map(index => { + const checkbox = event.currentTarget; + return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + }) + ); + }, + [indices, onChangeSelectedIndices] + ); + const choices = useMemo( () => - Object.keys(indices).map(indexName => ({ - id: indexName, - label: {indexName}, - })), - [indices] - ); + indices.map(index => { + const validIndex = index.errors.length === 0; + const checkbox = ( + {index.index}} + onChange={handleCheckboxChange} + checked={index.isSelected} + disabled={!validIndex} + /> + ); - const handleCheckboxGroupChange = useCallback( - indexName => { - onChangeSelectedIndices({ - ...indices, - [indexName]: !indices[indexName], - }); - }, - [indices, onChangeSelectedIndices] + return validIndex ? ( + checkbox + ) : ( +
+ {checkbox} +
+ ); + }), + [indices] ); return ( @@ -53,20 +74,17 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ /> } > - 0} - label={indicesSelectionLabel} - labelType="legend" - > - - + + + <>{choices} + + ); }; @@ -75,14 +93,50 @@ const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesS defaultMessage: 'Indices', }); -const formatValidationError = (validationError: IndicesValidationError) => { - switch (validationError) { - case 'TOO_FEW_SELECTED_INDICES': - return i18n.translate( - 'xpack.infra.analysisSetup.indicesSelectionTooFewSelectedIndicesDescription', - { - defaultMessage: 'Select at least one index name.', - } - ); - } +const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { + return errors.map(error => { + switch (error.error) { + case 'INDEX_NOT_FOUND': + return ( +

+ {error.index} }} + /> +

+ ); + + case 'FIELD_NOT_FOUND': + return ( +

+ {error.index}, + field: {error.field}, + }} + /> +

+ ); + + case 'FIELD_NOT_VALID': + return ( +

+ {error.index}, + field: {error.field}, + }} + /> +

+ ); + + default: + return ''; + } + }); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx index 929fba26f2323..3b5497fb91864 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx @@ -4,24 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiForm } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; -import { - AnalysisSetupIndicesForm, - IndicesSelection, - IndicesValidationError, -} from './analysis_setup_indices_form'; +import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; setEndTime: (endTime: number | undefined) => void; startTime: number | undefined; endTime: number | undefined; - selectedIndices: IndicesSelection; - setSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; + isValidating: boolean; + validatedIndices: ValidatedIndex[]; + setValidatedIndices: (selectedIndices: ValidatedIndex[]) => void; + validationErrors?: ValidationIndicesUIError[]; } export const InitialConfigurationStep: React.FunctionComponent = ({ @@ -29,16 +32,11 @@ export const InitialConfigurationStep: React.FunctionComponent { - const indicesFormValidationErrors = useMemo( - () => - validationErrors.filter(validationError => validationError === 'TOO_FEW_SELECTED_INDICES'), - [validationErrors] - ); - return ( <> @@ -50,11 +48,63 @@ export const InitialConfigurationStep: React.FunctionComponent + + ); }; + +const errorCalloutTitle = i18n.translate( + 'xpack.infra.analysisSetup.steps.initialConfigurationStep.errorCalloutTitle', + { + defaultMessage: 'Your index configuration is not valid', + } +); + +const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => { + if (errors.length === 0) { + return null; + } + + return ( + <> + +
    + {errors.map((error, i) => ( +
  • {formatValidationError(error)}
  • + ))} +
+
+ + + ); +}; + +const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => { + switch (error.error) { + case 'NETWORK_ERROR': + return ( + + ); + + case 'TOO_FEW_SELECTED_INDICES': + return ( + + ); + + default: + return ''; + } +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx index 27fc8a83bc086..978e45e26b733 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx @@ -60,8 +60,8 @@ export const ProcessStep: React.FunctionComponent = ({ defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist." /> - {errorMessages.map(errorMessage => ( - + {errorMessages.map((errorMessage, i) => ( + {errorMessage} ))} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx index aebb44d4c9372..4643516e73fac 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx @@ -25,6 +25,7 @@ interface AnalysisSetupStepsProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -34,6 +35,7 @@ export const AnalysisSetupSteps: React.FunctionComponent { const { @@ -43,13 +45,15 @@ export const AnalysisSetupSteps: React.FunctionComponent ), diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 98536f4c85d36..0093a6c21af57 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -13,7 +13,10 @@ import { createSnapshotResolvers } from './graphql/snapshot'; import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { InfraBackendLibs } from './lib/infra_types'; -import { initLogAnalysisGetLogEntryRateRoute } from './routes/log_analysis'; +import { + initLogAnalysisGetLogEntryRateRoute, + initIndexPatternsValidateRoute, +} from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetadataRoute } from './routes/metadata'; @@ -33,6 +36,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initIpToHostName(libs); initLogAnalysisGetLogEntryRateRoute(libs); + initIndexPatternsValidateRoute(libs); initMetricExplorerRoute(libs); initMetadataRoute(libs); }; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f0d26e5f5869f..63fded49d8222 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -45,7 +45,12 @@ export interface InfraBackendFrameworkAdapter { ): Promise; callWithRequest( req: InfraFrameworkRequest, - method: 'indices.getAlias' | 'indices.get', + method: 'indices.getAlias', + options?: object + ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: 'indices.get', options?: object ): Promise; callWithRequest( @@ -137,14 +142,32 @@ export interface InfraDatabaseMultiResponse extends InfraDatab } export interface InfraDatabaseFieldCapsResponse extends InfraDatabaseResponse { + indices: string[]; fields: InfraFieldsResponse; } +export interface InfraDatabaseGetIndicesAliasResponse { + [indexName: string]: { + aliases: { + [aliasName: string]: any; + }; + }; +} + export interface InfraDatabaseGetIndicesResponse { [indexName: string]: { aliases: { [aliasName: string]: any; }; + mappings: { + _meta: object; + dynamic_templates: any[]; + date_detection: boolean; + properties: { + [fieldName: string]: any; + }; + }; + settings: { index: object }; }; } diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts index 38684cb22e237..7364d167efe47 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './index_patterns'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts new file mode 100644 index 0000000000000..a85e119e7318a --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './validate'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts new file mode 100644 index 0000000000000..0a369adb7ca29 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.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 Boom from 'boom'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, + ValidationIndicesError, +} from '../../../../common/http_api'; + +import { throwErrors } from '../../../../common/runtime_types'; + +const partitionField = 'event.dataset'; + +export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { + framework.registerRoute({ + method: 'POST', + path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + handler: async (req, res) => { + const payload = pipe( + validationIndicesRequestPayloadRT.decode(req.payload), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { timestampField, indices } = payload.data; + const errors: ValidationIndicesError[] = []; + + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async index => { + const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', { + index, + fields: `${timestampField},${partitionField}`, + }); + + if (fieldCaps.indices.length === 0) { + errors.push({ + error: 'INDEX_NOT_FOUND', + index, + }); + return; + } + + ([ + [timestampField, 'date'], + [partitionField, 'keyword'], + ] as const).forEach(([field, fieldType]) => { + const fieldMetadata = fieldCaps.fields[field]; + + if (fieldMetadata === undefined) { + errors.push({ + error: 'FIELD_NOT_FOUND', + index, + field, + }); + } else { + const fieldTypes = Object.keys(fieldMetadata); + + if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + errors.push({ + error: `FIELD_NOT_VALID`, + index, + field, + }); + } + } + }); + }) + ); + + return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } })); + }, + }); +}; From b136bfdb82266e1f1265e70658f01788fdda4aeb Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 25 Nov 2019 10:19:24 -0700 Subject: [PATCH 10/52] [File upload][Maps] NP migration for server & client (#51045) * Use np savedObjectsClient in indexing service * Export File Upload UI via start since it requires initialization * Pass services through top level react component props * Handle basePath ref and 'kbn-version' for requests * Bulk of logic for removing hapi server dependencies for server app * Use request obj subset of original request * Move startup logic over to server plugin file and call from index.js * Update server tests * Clean up * Remove old makeUsageCollector export statement * initServicesAndConstants in the start method instead of in the react component * Review feedback --- .../common/constants/file_import.ts | 2 + x-pack/legacy/plugins/file_upload/index.js | 29 ++++++++--- .../legacy/plugins/file_upload/mappings.json | 9 ---- x-pack/legacy/plugins/file_upload/mappings.ts | 15 ++++++ .../public/components/json_import_progress.js | 4 +- .../file_upload/public/{index.js => index.ts} | 6 ++- .../file_upload/public/kibana_services.js | 13 +++++ .../plugins/file_upload/public/legacy.ts | 12 +++++ .../plugins/file_upload/public/plugin.ts | 33 ++++++++++++ .../file_upload/public/util/http_service.js | 4 +- .../public/util/indexing_service.js | 24 ++++----- .../call_with_internal_user_factory.d.ts | 4 +- .../client/call_with_internal_user_factory.js | 9 ++-- .../call_with_internal_user_factory.test.ts | 22 +++----- .../client/call_with_request_factory.js | 8 +-- .../plugins/file_upload/server/plugin.js | 36 +++++++++++++ .../file_upload/server/routes/file_upload.js | 50 +++++++++++-------- .../file_upload/server/telemetry/index.ts | 1 - .../server/telemetry/make_usage_collector.ts | 27 ---------- .../server/telemetry/telemetry.test.ts | 22 ++++---- .../file_upload/server/telemetry/telemetry.ts | 33 ++++++++---- .../create_client_file_source_editor.js | 4 +- 22 files changed, 229 insertions(+), 138 deletions(-) delete mode 100644 x-pack/legacy/plugins/file_upload/mappings.json create mode 100644 x-pack/legacy/plugins/file_upload/mappings.ts rename x-pack/legacy/plugins/file_upload/public/{index.js => index.ts} (69%) create mode 100644 x-pack/legacy/plugins/file_upload/public/legacy.ts create mode 100644 x-pack/legacy/plugins/file_upload/public/plugin.ts create mode 100644 x-pack/legacy/plugins/file_upload/server/plugin.js delete mode 100644 x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts diff --git a/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts b/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts index 1c82c2b6237e1..0770899af5393 100644 --- a/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts +++ b/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts @@ -16,3 +16,5 @@ export const ES_GEO_FIELD_TYPE = { GEO_POINT: 'geo_point', GEO_SHAPE: 'geo_shape', }; + +export const DEFAULT_KBN_VERSION = 'kbnVersion'; diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index 24907082adb2c..37d4ad80fa2ca 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { fileUploadRoutes } from './server/routes/file_upload'; -import { makeUsageCollector } from './server/telemetry/'; -import mappings from './mappings'; +import { FileUploadPlugin } from './server/plugin'; +import { mappings } from './mappings'; export const fileUpload = kibana => { return new kibana.Plugin({ @@ -23,11 +21,26 @@ export const fileUpload = kibana => { }, init(server) { - const { xpack_main: xpackMainPlugin } = server.plugins; + const coreSetup = server.newPlatform.setup.core; + const pluginsSetup = {}; - mirrorPluginStatus(xpackMainPlugin, this); - fileUploadRoutes(server); - makeUsageCollector(server); + // legacy dependencies + const __LEGACY = { + route: server.route.bind(server), + plugins: { + elasticsearch: server.plugins.elasticsearch, + }, + savedObjects: { + getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository + }, + usage: { + collectorSet: { + makeUsageCollector: server.usage.collectorSet.makeUsageCollector + } + } + }; + + new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); } }); }; diff --git a/x-pack/legacy/plugins/file_upload/mappings.json b/x-pack/legacy/plugins/file_upload/mappings.json deleted file mode 100644 index addff6308d3f0..0000000000000 --- a/x-pack/legacy/plugins/file_upload/mappings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - } -} diff --git a/x-pack/legacy/plugins/file_upload/mappings.ts b/x-pack/legacy/plugins/file_upload/mappings.ts new file mode 100644 index 0000000000000..70229c7088324 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/mappings.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mappings = { + 'file-upload-telemetry': { + properties: { + filesUploadedTotalCount: { + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js b/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js index 9e553a536845d..9c6248049d9cf 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js @@ -8,7 +8,7 @@ import React, { Fragment, Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTitle, EuiProgress, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; +import { basePath } from '../kibana_services'; export class JsonImportProgress extends Component { @@ -114,7 +114,7 @@ export class JsonImportProgress extends Component { diff --git a/x-pack/legacy/plugins/file_upload/public/index.js b/x-pack/legacy/plugins/file_upload/public/index.ts similarity index 69% rename from x-pack/legacy/plugins/file_upload/public/index.js rename to x-pack/legacy/plugins/file_upload/public/index.ts index a02b82170f70f..205ceae37d6a1 100644 --- a/x-pack/legacy/plugins/file_upload/public/index.js +++ b/x-pack/legacy/plugins/file_upload/public/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { JsonUploadAndParse } from './components/json_upload_and_parse'; +import { FileUploadPlugin } from './plugin'; + +export function plugin() { + return new FileUploadPlugin(); +} diff --git a/x-pack/legacy/plugins/file_upload/public/kibana_services.js b/x-pack/legacy/plugins/file_upload/public/kibana_services.js index 10a6ae7179bc2..3c00ab5709660 100644 --- a/x-pack/legacy/plugins/file_upload/public/kibana_services.js +++ b/x-pack/legacy/plugins/file_upload/public/kibana_services.js @@ -5,5 +5,18 @@ */ import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { DEFAULT_KBN_VERSION } from '../common/constants/file_import'; export const indexPatternService = data.indexPatterns.indexPatterns; + +export let savedObjectsClient; +export let basePath; +export let apiBasePath; +export let kbnVersion; + +export const initServicesAndConstants = ({ savedObjects, http, injectedMetadata }) => { + savedObjectsClient = savedObjects.client; + basePath = http.basePath.basePath; + apiBasePath = http.basePath.prepend('/api'); + kbnVersion = injectedMetadata.getKibanaVersion(DEFAULT_KBN_VERSION); +}; diff --git a/x-pack/legacy/plugins/file_upload/public/legacy.ts b/x-pack/legacy/plugins/file_upload/public/legacy.ts new file mode 100644 index 0000000000000..719599df3ccbe --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/legacy.ts @@ -0,0 +1,12 @@ +/* + * 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 { npStart } from 'ui/new_platform'; +import { plugin } from '.'; + +const pluginInstance = plugin(); + +export const start = pluginInstance.start(npStart.core); diff --git a/x-pack/legacy/plugins/file_upload/public/plugin.ts b/x-pack/legacy/plugins/file_upload/public/plugin.ts new file mode 100644 index 0000000000000..cc9ebbfc15b39 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreStart } from 'src/core/public'; +// @ts-ignore +import { initResources } from './util/indexing_service'; +// @ts-ignore +import { JsonUploadAndParse } from './components/json_upload_and_parse'; +// @ts-ignore +import { initServicesAndConstants } from './kibana_services'; + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type FileUploadPluginSetup = ReturnType; +export type FileUploadPluginStart = ReturnType; + +/** @internal */ +export class FileUploadPlugin implements Plugin { + public setup() {} + + public start(core: CoreStart) { + initServicesAndConstants(core); + return { + JsonUploadAndParse, + }; + } +} diff --git a/x-pack/legacy/plugins/file_upload/public/util/http_service.js b/x-pack/legacy/plugins/file_upload/public/util/http_service.js index 26d46cecb0e51..a744f0f075490 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/http_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/http_service.js @@ -6,9 +6,9 @@ // service for interacting with the server -import chrome from 'ui/chrome'; import { addSystemApiHeader } from 'ui/system_api'; import { i18n } from '@kbn/i18n'; +import { kbnVersion } from '../kibana_services'; export async function http(options) { if(!(options && options.url)) { @@ -20,7 +20,7 @@ export async function http(options) { const url = options.url || ''; const headers = addSystemApiHeader({ 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), + 'kbn-version': kbnVersion, ...options.headers }); diff --git a/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js b/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js index fd96bd95d4bb7..b40659ec4b513 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { http } from './http_service'; -import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { indexPatternService } from '../kibana_services'; +import { http as httpService } from './http_service'; +import { + indexPatternService, + apiBasePath, + savedObjectsClient +} from '../kibana_services'; import { getGeoJsonIndexingDetails } from './geo_processing'; import { sizeLimitedChunking } from './size_limited_chunking'; +import { i18n } from '@kbn/i18n'; -const basePath = chrome.addBasePath('/api/fileupload'); const fileType = 'json'; export async function indexData(parsedFile, transformDetails, indexName, dataType, appName) { @@ -19,7 +21,6 @@ export async function indexData(parsedFile, transformDetails, indexName, dataTyp throw(i18n.translate('xpack.fileUpload.indexingService.noFileImported', { defaultMessage: 'No file imported.' })); - return; } // Perform any processing required on file prior to indexing @@ -129,8 +130,8 @@ async function writeToIndex(indexingDetails) { ingestPipeline } = indexingDetails; - return await http({ - url: `${basePath}/import${paramString}`, + return await httpService({ + url: `${apiBasePath}/fileupload/import${paramString}`, method: 'POST', data: { index, @@ -223,7 +224,6 @@ export async function createIndexPattern(indexPatternName) { } async function getIndexPatternId(name) { - const savedObjectsClient = chrome.getSavedObjectsClient(); const savedObjectSearch = await savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); const indexPatternSavedObjects = savedObjectSearch.savedObjects; @@ -237,9 +237,8 @@ async function getIndexPatternId(name) { } export const getExistingIndexNames = async () => { - const basePath = chrome.addBasePath('/api'); - const indexes = await http({ - url: `${basePath}/index_management/indices`, + const indexes = await httpService({ + url: `${apiBasePath}/index_management/indices`, method: 'GET', }); return indexes @@ -248,7 +247,6 @@ export const getExistingIndexNames = async () => { }; export const getExistingIndexPatternNames = async () => { - const savedObjectsClient = chrome.getSavedObjectsClient(); const indexPatterns = await savedObjectsClient.find({ type: 'index-pattern', fields: ['id', 'title', 'type', 'fields'], diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts index 0b39c81cee6ff..9c1000db8cb56 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; - -export function callWithInternalUserFactory(server: Server): any; +export function callWithInternalUserFactory(elasticsearchPlugin: any): any; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js index dc3131484e75f..f42c3ffb99a5b 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js @@ -5,16 +5,15 @@ */ - import { once } from 'lodash'; -const _callWithInternalUser = once((server) => { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); +const _callWithInternalUser = once(elasticsearchPlugin => { + const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); return callWithInternalUser; }); -export const callWithInternalUserFactory = (server) => { +export const callWithInternalUserFactory = elasticsearchPlugin => { return (...args) => { - return _callWithInternalUser(server)(...args); + return _callWithInternalUser(elasticsearchPlugin)(...args); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts index d77541e7d3d6c..04c5013ed8e67 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts @@ -8,25 +8,15 @@ import { callWithInternalUserFactory } from './call_with_internal_user_factory'; describe('call_with_internal_user_factory', () => { describe('callWithInternalUserFactory', () => { - let server: any; - let callWithInternalUser: any; - - beforeEach(() => { - callWithInternalUser = jest.fn(); - server = { - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, - }; - }); - it('should use internal user "admin"', () => { - const callWithInternalUserInstance = callWithInternalUserFactory(server); + const callWithInternalUser: any = jest.fn(); + const elasticsearchPlugin: any = { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }; + const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); callWithInternalUserInstance(); - expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('admin'); + expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); }); }); }); diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js index 0040fcb6c802a..885573c993b7f 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js @@ -8,13 +8,13 @@ import { once } from 'lodash'; -const callWithRequest = once((server) => { - const cluster = server.plugins.elasticsearch.getCluster('data'); +const callWithRequest = once(elasticsearchPlugin => { + const cluster = elasticsearchPlugin.getCluster('data'); return cluster.callWithRequest; }); -export const callWithRequestFactory = (server, request) => { +export const callWithRequestFactory = (elasticsearchPlugin, request) => { return (...args) => { - return callWithRequest(server)(request, ...args); + return callWithRequest(elasticsearchPlugin)(request, ...args); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js new file mode 100644 index 0000000000000..0baef6f8ffa40 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getImportRouteHandler } from './routes/file_upload'; +import { getTelemetry, initTelemetry } from './telemetry/telemetry'; +import { MAX_BYTES } from '../common/constants/file_import'; + +const TELEMETRY_TYPE = 'fileUploadTelemetry'; + +export class FileUploadPlugin { + setup(core, plugins, __LEGACY) { + const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; + const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; + const makeUsageCollector = __LEGACY.usage.collectorSet.makeUsageCollector; + + // Set up route + __LEGACY.route({ + method: 'POST', + path: '/api/fileupload/import', + handler: getImportRouteHandler(elasticsearchPlugin, getSavedObjectsRepository), + config: { + payload: { maxBytes: MAX_BYTES }, + } + }); + + // Make usage collector + makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry() + }); + } +} diff --git a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js index ac07d80962bdc..1eeecdeb1525b 100644 --- a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js +++ b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js @@ -7,7 +7,6 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; import { importDataProvider } from '../models/import_data'; -import { MAX_BYTES } from '../../common/constants/file_import'; import { updateTelemetry } from '../telemetry/telemetry'; @@ -18,28 +17,35 @@ function importData({ return importDataFunc(id, index, settings, mappings, ingestPipeline, data); } -export function fileUploadRoutes(server, commonRouteConfig) { +export function getImportRouteHandler(elasticsearchPlugin, getSavedObjectsRepository) { + return async request => { - server.route({ - method: 'POST', - path: '/api/fileupload/import', - async handler(request) { + const requestObj = { + query: request.query, + payload: request.payload, + params: request.payload, + auth: request.auth, + headers: request.headers + }; - // `id` being `undefined` tells us that this is a new import due to create a new index. - // follow-up import calls to just add additional data will include the `id` of the created - // index, we'll ignore those and don't increment the counter. - const { id } = request.query; - if (id === undefined) { - await updateTelemetry({ server, ...request.payload }); - } - - const callWithRequest = callWithRequestFactory(server, request); - return importData({ callWithRequest, id, ...request.payload }) - .catch(wrapError); - }, - config: { - ...commonRouteConfig, - payload: { maxBytes: MAX_BYTES }, + // `id` being `undefined` tells us that this is a new import due to create a new index. + // follow-up import calls to just add additional data will include the `id` of the created + // index, we'll ignore those and don't increment the counter. + const { id } = requestObj.query; + if (id === undefined) { + await updateTelemetry({ elasticsearchPlugin, getSavedObjectsRepository }); } - }); + + const requestContentWithDefaults = { + id, + callWithRequest: callWithRequestFactory(elasticsearchPlugin, requestObj), + index: undefined, + settings: {}, + mappings: {}, + ingestPipeline: {}, + data: [], + ...requestObj.payload + }; + return importData(requestContentWithDefaults).catch(wrapError); + }; } diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts index d05f7cc63c896..46da040dc34f0 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts @@ -5,4 +5,3 @@ */ export * from './telemetry'; -export { makeUsageCollector } from './make_usage_collector'; diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts deleted file mode 100644 index f589280d8cf3a..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts +++ /dev/null @@ -1,27 +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 { Server } from 'hapi'; -import { getTelemetry, initTelemetry, Telemetry } from './telemetry'; - -// TODO this type should be defined by the platform -interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - register: any; - }; - }; -} - -export function makeUsageCollector(server: KibanaHapiServer): void { - const fileUploadUsageCollector = server.usage.collectorSet.makeUsageCollector({ - type: 'fileUploadTelemetry', - isReady: () => true, - fetch: async (): Promise => (await getTelemetry(server)) || initTelemetry(), - }); - server.usage.collectorSet.register(fileUploadUsageCollector); -} diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts index 5017c9cb41f08..1c785d8e7b61c 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts @@ -6,22 +6,13 @@ import { getTelemetry, updateTelemetry } from './telemetry'; +const elasticsearchPlugin: any = null; +const getSavedObjectsRepository: any = null; const internalRepository = () => ({ get: jest.fn(() => null), create: jest.fn(() => ({ attributes: 'test' })), update: jest.fn(() => ({ attributes: 'test' })), }); -const server: any = { - savedObjects: { - getSavedObjectsRepository: jest.fn(() => internalRepository()), - }, - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, -}; -const callWithInternalUser = jest.fn(); function mockInit(getVal: any = { attributes: {} }): any { return { @@ -34,7 +25,7 @@ describe('file upload plugin telemetry', () => { describe('getTelemetry', () => { it('should get existing telemetry', async () => { const internalRepo = mockInit(); - await getTelemetry(server, internalRepo); + await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository, internalRepo); expect(internalRepo.update.mock.calls.length).toBe(0); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); @@ -48,7 +39,12 @@ describe('file upload plugin telemetry', () => { filesUploadedTotalCount: 2, }, }); - await updateTelemetry({ server, internalRepo }); + + await updateTelemetry({ + elasticsearchPlugin, + getSavedObjectsRepository, + internalRepo, + }); expect(internalRepo.update.mock.calls.length).toBe(1); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts index b43e2a1b33a29..5ffa735f4c83a 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import _ from 'lodash'; import { callWithInternalUserFactory } from '../client/call_with_internal_user_factory'; @@ -18,9 +17,11 @@ export interface TelemetrySavedObject { attributes: Telemetry; } -export function getInternalRepository(server: Server): any { - const { getSavedObjectsRepository } = server.savedObjects; - const callWithInternalUser = callWithInternalUserFactory(server); +export function getInternalRepository( + elasticsearchPlugin: any, + getSavedObjectsRepository: any +): any { + const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); return getSavedObjectsRepository(callWithInternalUser); } @@ -30,8 +31,13 @@ export function initTelemetry(): Telemetry { }; } -export async function getTelemetry(server: Server, internalRepo?: object): Promise { - const internalRepository = internalRepo || getInternalRepository(server); +export async function getTelemetry( + elasticsearchPlugin: any, + getSavedObjectsRepository: any, + internalRepo?: object +): Promise { + const internalRepository = + internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); let telemetrySavedObject; try { @@ -44,14 +50,21 @@ export async function getTelemetry(server: Server, internalRepo?: object): Promi } export async function updateTelemetry({ - server, + elasticsearchPlugin, + getSavedObjectsRepository, internalRepo, }: { - server: any; + elasticsearchPlugin: any; + getSavedObjectsRepository: any; internalRepo?: any; }) { - const internalRepository = internalRepo || getInternalRepository(server); - let telemetry = await getTelemetry(server, internalRepository); + const internalRepository = + internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); + let telemetry = await getTelemetry( + elasticsearchPlugin, + getSavedObjectsRepository, + internalRepository + ); // Create if doesn't exist if (!telemetry || _.isEmpty(telemetry)) { const newTelemetrySavedObject = await internalRepository.create( diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js index 7c56f4c7f4316..6e931ebe2b95b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js @@ -6,7 +6,7 @@ import React from 'react'; -import { JsonUploadAndParse } from '../../../../../file_upload/public'; +import { start as fileUpload } from '../../../../../file_upload/public/legacy'; export function ClientFileCreateSourceEditor({ previewGeojsonFile, @@ -16,7 +16,7 @@ export function ClientFileCreateSourceEditor({ onIndexReady, }) { return ( - Date: Mon, 25 Nov 2019 18:28:49 +0100 Subject: [PATCH 11/52] [Console] Refactoring more legacy code and implementing minor fixes (#51312) * First part of context refactor * Finised "hook"ing in to new context for old editor output. Also fixed passing through of content type * Remove comment * - update console history behaviour - don't scroll into view on click. Doesn't really make sense. - make triple quote setting update in place --- .eslintrc.js | 6 - .../application/components/editor_example.tsx | 2 +- .../split_panel/containers/panel.tsx | 2 +- .../console_history/console_history.tsx | 58 ++++----- .../console_history/history_viewer.tsx | 8 +- .../legacy => }/console_history/index.ts | 0 .../containers/editor/context/reducer.ts | 77 ----------- .../application/containers/editor/editor.tsx | 64 +++++++++ .../application/containers/editor/index.ts | 4 +- .../legacy/console_editor/editor.test.tsx | 41 ++++-- .../editor/legacy/console_editor/editor.tsx | 45 +++---- .../legacy/console_editor/editor_output.tsx | 59 +++++++-- .../containers/editor/legacy/index.ts | 1 - .../legacy/use_ui_ace_keyboard_mode.tsx | 57 ++++----- .../application/containers/main/main.tsx | 49 ++----- .../application/containers/settings.tsx | 7 +- .../contexts/create_use_context.ts | 30 +++++ .../editor_context}/editor_context.tsx | 42 ++---- .../editor_context}/editor_registry.ts | 9 -- .../editor_context}/index.ts | 0 .../public/application/contexts/index.ts | 32 +++++ .../application/contexts/request_context.tsx | 40 ++++++ .../services_context.tsx} | 12 +- .../public/application/hooks/index.ts | 22 ++++ .../use_restore_request_from_history/index.ts | 20 +++ .../restore_request_from_history.ts | 4 + .../use_restore_request_from_history.ts | 28 ++++ .../use_send_current_request_to_es}/index.ts | 2 +- .../send_request_to_es.ts} | 121 ++++++------------ .../use_send_current_request_to_es.ts | 85 ++++++++++++ .../application/hooks/use_set_input_editor.ts | 30 +++++ .../np_ready/public/application/index.tsx | 17 +-- .../public/application/stores/editor.ts | 57 +++++++++ .../public/application/stores/request.ts | 71 ++++++++++ .../console/np_ready/public/types/common.ts | 26 ++++ 35 files changed, 737 insertions(+), 391 deletions(-) rename src/legacy/core_plugins/console/np_ready/public/application/containers/{editor/legacy => }/console_history/console_history.tsx (87%) rename src/legacy/core_plugins/console/np_ready/public/application/containers/{editor/legacy => }/console_history/history_viewer.tsx (86%) rename src/legacy/core_plugins/console/np_ready/public/application/containers/{editor/legacy => }/console_history/index.ts (100%) delete mode 100644 src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/context => contexts/editor_context}/editor_context.tsx (51%) rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/context => contexts/editor_context}/editor_registry.ts (87%) rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/context => contexts/editor_context}/index.ts (100%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx rename src/legacy/core_plugins/console/np_ready/public/application/{context/app_context.tsx => contexts/services_context.tsx} (77%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/legacy/console_history => hooks/use_restore_request_from_history}/restore_request_from_history.ts (90%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts rename src/legacy/core_plugins/console/np_ready/public/application/{context => hooks/use_send_current_request_to_es}/index.ts (91%) rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/legacy/console_editor/send_current_request_to_es.ts => hooks/use_send_current_request_to_es/send_request_to_es.ts} (50%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/types/common.ts diff --git a/.eslintrc.js b/.eslintrc.js index 5b7dd6d6d0379..fe546ec02a668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,12 +64,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/legacy/core_plugins/console/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/legacy/core_plugins/data/**/*.{js,ts,tsx}'], rules: { diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx index 33cefd9b20968..01bd3fcd78e53 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx @@ -41,7 +41,7 @@ export function EditorExample(props: EditorExampleProps) { return () => { editor.destroy(); }; - }, []); + }, [elemId]); return
; } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx index 747c21433f8ed..80960a7772ba1 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx @@ -41,7 +41,7 @@ export function Panel({ children, initialWidth = '100%', style = {} }: Props) { return divRef.current!.getBoundingClientRect().width; }, }); - }, []); + }, [initialWidth, registry]); return (
diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx similarity index 87% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx index fdfe9ecc7b94c..30966a2f77e1d 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import moment from 'moment'; @@ -32,9 +32,10 @@ import { EuiButton, } from '@elastic/eui'; -import { useAppContext } from '../../../../context'; +import { useServicesContext } from '../../contexts'; import { HistoryViewer } from './history_viewer'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; +import { useEditorReadContext } from '../../contexts/editor_context'; +import { useRestoreRequestFromHistory } from '../../hooks'; interface Props { close: () => void; @@ -45,9 +46,8 @@ const CHILD_ELEMENT_PREFIX = 'historyReq'; export function ConsoleHistory({ close }: Props) { const { services: { history }, - } = useAppContext(); + } = useServicesContext(); - const dispatch = useEditorActionContext(); const { settings: readOnlySettings } = useEditorReadContext(); const [requests, setPastRequests] = useState(history.getHistory()); @@ -55,7 +55,7 @@ export function ConsoleHistory({ close }: Props) { const clearHistory = useCallback(() => { history.clearHistory(); setPastRequests(history.getHistory()); - }, []); + }, [history]); const listRef = useRef(null); @@ -63,14 +63,7 @@ export function ConsoleHistory({ close }: Props) { const [selectedIndex, setSelectedIndex] = useState(0); const selectedReq = useRef(null); - const scrollIntoView = (idx: number) => { - const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); - if (activeDescendant) { - activeDescendant.scrollIntoView(); - } - }; - - const [describeReq] = useState(() => { + const describeReq = useMemo(() => { const _describeReq = (req: any) => { const endpoint = req.endpoint; const date = moment(req.time); @@ -86,34 +79,39 @@ export function ConsoleHistory({ close }: Props) { (_describeReq as any).cache = new WeakMap(); return memoize(_describeReq); - }); + }, []); + + const scrollIntoView = useCallback((idx: number) => { + const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); + if (activeDescendant) { + activeDescendant.scrollIntoView(); + } + }, []); - const initialize = () => { + const initialize = useCallback(() => { const nextSelectedIndex = 0; (describeReq as any).cache = new WeakMap(); setViewingReq(requests[nextSelectedIndex]); selectedReq.current = requests[nextSelectedIndex]; setSelectedIndex(nextSelectedIndex); scrollIntoView(nextSelectedIndex); - }; + }, [describeReq, requests, scrollIntoView]); const clear = () => { clearHistory(); initialize(); }; - const restore = (req: any = selectedReq.current) => { - dispatch({ type: 'restoreRequest', value: req }); - }; + const restoreRequestFromHistory = useRestoreRequestFromHistory(); useEffect(() => { initialize(); - }, [requests]); + }, [initialize]); useEffect(() => { const done = history.change(setPastRequests); return () => done(); - }, []); + }, [history]); /* eslint-disable */ return ( @@ -128,7 +126,7 @@ export function ConsoleHistory({ close }: Props) { ref={listRef} onKeyDown={(ev: React.KeyboardEvent) => { if (ev.keyCode === keyCodes.ENTER) { - restore(); + restoreRequestFromHistory(selectedReq.current); return; } @@ -173,12 +171,11 @@ export function ConsoleHistory({ close }: Props) { setViewingReq(req); selectedReq.current = req; setSelectedIndex(idx); - scrollIntoView(idx); }} role="option" onMouseEnter={() => setViewingReq(req)} onMouseLeave={() => setViewingReq(selectedReq.current)} - onDoubleClick={() => restore(req)} + onDoubleClick={restoreRequestFromHistory} aria-label={i18n.translate('console.historyPage.itemOfRequestListAriaLabel', { defaultMessage: 'Request: {historyItem}', values: { historyItem: reqDescription }, @@ -196,10 +193,7 @@ export function ConsoleHistory({ close }: Props) {
- +
@@ -224,7 +218,11 @@ export function ConsoleHistory({ close }: Props) { - restore()}> + restoreRequestFromHistory(selectedReq.current)} + > {i18n.translate('console.historyPage.applyHistoryButtonLabel', { defaultMessage: 'Apply', })} diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx similarity index 86% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx index c15bec0563049..6fbb46bba6212 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx @@ -21,12 +21,12 @@ import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import $ from 'jquery'; -import { DevToolsSettings } from '../../../../../services'; -import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; +import { DevToolsSettings } from '../../../services'; +import { subscribeResizeChecker } from '../editor/legacy/subscribe_console_resize_checker'; // @ts-ignore -import SenseEditor from '../../../../../../../public/quarantined/src/sense_editor/editor'; -import { applyCurrentSettings } from '../console_editor/apply_editor_settings'; +import SenseEditor from '../../../../../public/quarantined/src/sense_editor/editor'; +import { applyCurrentSettings } from '../editor/legacy/console_editor/apply_editor_settings'; interface Props { settings: DevToolsSettings; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/index.ts similarity index 100% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/index.ts diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts deleted file mode 100644 index caed6b24c3c11..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts +++ /dev/null @@ -1,77 +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 { Reducer } from 'react'; - -import { instance as registry } from './editor_registry'; -import { ContextValue } from './editor_context'; - -import { restoreRequestFromHistory } from '../legacy/console_history/restore_request_from_history'; -import { - sendCurrentRequestToES, - EsRequestArgs, -} from '../legacy/console_editor/send_current_request_to_es'; -import { DevToolsSettings } from '../../../../services'; - -export type Action = - | { type: 'setInputEditor'; value: any } - | { type: 'setOutputEditor'; value: any } - | { type: 'restoreRequest'; value: any } - | { type: 'updateSettings'; value: DevToolsSettings } - | { type: 'sendRequestToEs'; value: EsRequestArgs } - | { type: 'updateRequestHistory'; value: any }; - -export const reducer: Reducer = (state, action) => { - const nextState = { ...state }; - - if (action.type === 'setInputEditor') { - registry.setInputEditor(action.value); - if (registry.getOutputEditor()) { - nextState.editorsReady = true; - } - } - - if (action.type === 'setOutputEditor') { - registry.setOutputEditor(action.value); - if (registry.getInputEditor()) { - nextState.editorsReady = true; - } - } - - if (action.type === 'restoreRequest') { - restoreRequestFromHistory(registry.getInputEditor(), action.value); - } - - if (action.type === 'updateSettings') { - nextState.settings = action.value; - } - - if (action.type === 'sendRequestToEs') { - const { callback, isPolling, isUsingTripleQuotes } = action.value; - sendCurrentRequestToES({ - input: registry.getInputEditor(), - output: registry.getOutputEditor(), - callback, - isUsingTripleQuotes, - isPolling, - }); - } - - return nextState; -}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx new file mode 100644 index 0000000000000..07b48c083bf61 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useCallback } from 'react'; +import { debounce } from 'lodash'; + +import { Panel, PanelsContainer } from '../../components/split_panel'; +import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; +import { StorageKeys } from '../../../services'; +import { useServicesContext } from '../../contexts'; + +const INITIAL_PANEL_WIDTH = 50; +const PANEL_MIN_WIDTH = '100px'; + +export const Editor = () => { + const { + services: { storage }, + } = useServicesContext(); + + const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ + INITIAL_PANEL_WIDTH, + INITIAL_PANEL_WIDTH, + ]); + + const onPanelWidthChange = useCallback( + debounce((widths: number[]) => { + storage.set(StorageKeys.WIDTH, widths); + }, 300), + [] + ); + + return ( + + + + + + + + + ); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts index b3cab3d13b3a3..87436d7f97389 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { Editor, EditorOutput, ConsoleHistory, autoIndent, getDocumentation } from './legacy'; -export { useEditorActionContext, useEditorReadContext, EditorContextProvider } from './context'; +export { autoIndent, getDocumentation } from './legacy'; +export { Editor } from './editor'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx index 03d5b3f1d8f44..cb5559edfb249 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -20,15 +20,29 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; +import { act } from 'react-dom/test-utils'; import * as sinon from 'sinon'; -import { EditorContextProvider } from '../../context'; -import { AppContextProvider } from '../../../../context'; +import { + ServicesContextProvider, + EditorContextProvider, + RequestContextProvider, +} from '../../../../contexts'; + import { Editor } from './editor'; +jest.mock('../../../../contexts/editor_context/editor_registry.ts', () => ({ + instance: { + setInputEditor: () => {}, + getInputEditor: () => ({ + getRequestsInRange: (cb: any) => cb([{ test: 'test' }]), + }), + }, +})); jest.mock('../../../../components/editor_example.tsx', () => {}); jest.mock('../../../../../../../public/quarantined/src/mappings.js', () => ({ retrieveAutoCompleteInfo: () => {}, + clearSubscriptions: () => {}, })); jest.mock('../../../../../../../public/quarantined/src/input.ts', () => { return { @@ -46,7 +60,7 @@ jest.mock('../../../../../../../public/quarantined/src/input.ts', () => { }; }); -import * as sendRequestModule from './send_current_request_to_es'; +import * as sendRequestModule from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; import * as consoleMenuActions from '../console_menu_actions'; describe('Legacy (Ace) Console Editor Component Smoke Test', () => { @@ -66,19 +80,24 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { }; editor = mount( - - - - - + + + + + + + ); }); - it('calls send current request to ES', () => { - const stub = sinon.stub(sendRequestModule, 'sendCurrentRequestToES'); + // TODO: Re-enable when React ^16.9 is available + it.skip('calls send current request to ES', () => { + const stub = sinon.stub(sendRequestModule, 'sendRequestToES'); try { - editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + act(() => { + editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + }); expect(stub.called).toBe(true); expect(stub.callCount).toBe(1); } finally { diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx index 10f1ef34602a6..0fa0ec732c770 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import $ from 'jquery'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useAppContext } from '../../../../context'; +import { useServicesContext, useEditorReadContext } from '../../../../contexts'; import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode'; import { ConsoleMenu } from '../../../../components'; @@ -32,12 +32,13 @@ import { autoIndent, getDocumentation } from '../console_menu_actions'; import { registerCommands } from './keyboard_shortcuts'; import { applyCurrentSettings } from './apply_editor_settings'; +import { useSendCurrentRequestToES, useSetInputEditor } from '../../../../hooks'; + // @ts-ignore import { initializeEditor } from '../../../../../../../public/quarantined/src/input'; // @ts-ignore import mappings from '../../../../../../../public/quarantined/src/mappings'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { loadRemoteState } from './load_remote_editor_state'; @@ -60,14 +61,15 @@ const DEFAULT_INPUT_VALUE = `GET _search } }`; -function _Editor({ previousStateLocation = 'stored' }: EditorProps) { +function EditorUI({ previousStateLocation = 'stored' }: EditorProps) { const { services: { history, notifications }, docLinkVersion, - } = useAppContext(); + } = useServicesContext(); const { settings } = useEditorReadContext(); - const dispatch = useEditorActionContext(); + const setInputEditor = useSetInputEditor(); + const sendCurrentRequestToES = useSendCurrentRequestToES(); const editorRef = useRef(null); const actionsRef = useRef(null); @@ -76,13 +78,13 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { const [textArea, setTextArea] = useState(null); useUIAceKeyboardMode(textArea); - const openDocumentation = async () => { + const openDocumentation = useCallback(async () => { const documentation = await getDocumentation(editorInstanceRef.current!, docLinkVersion); if (!documentation) { return; } window.open(documentation, '_blank'); - }; + }, [docLinkVersion]); useEffect(() => { const $editor = $(editorRef.current!); @@ -102,7 +104,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { let timer: number; const saveDelay = 500; - return editorInstanceRef.current.getSession().on('change', function onChange() { + editorInstanceRef.current.getSession().on('change', function onChange() { if (timer) { clearTimeout(timer); } @@ -119,11 +121,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { } } - dispatch({ - type: 'setInputEditor', - value: editorInstanceRef.current, - }); - + setInputEditor(editorInstanceRef.current); setTextArea(editorRef.current!.querySelector('textarea')); mappings.retrieveAutoCompleteInfo(); @@ -132,26 +130,13 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { editorRef.current!, editorInstanceRef.current ); - const unsubscribeAutoSave = setupAutosave(); + setupAutosave(); return () => { unsubscribeResizer(); - unsubscribeAutoSave(); mappings.clearSubscriptions(); }; - }, []); - - const sendCurrentRequestToES = useCallback(() => { - dispatch({ - type: 'sendRequestToEs', - value: { - isUsingTripleQuotes: settings.tripleQuotes, - isPolling: settings.polling, - callback: (esPath: any, esMethod: any, esData: any) => - history.addToHistory(esPath, esMethod, esData), - }, - }); - }, [settings]); + }, [history, previousStateLocation, setInputEditor]); useEffect(() => { applyCurrentSettings(editorInstanceRef.current!, settings); @@ -165,7 +150,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { sendCurrentRequestToES, openDocumentation, }); - }, [sendCurrentRequestToES]); + }, [sendCurrentRequestToES, openDocumentation]); return (
@@ -219,4 +204,4 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { ); } -export const Editor = React.memo(_Editor); +export const Editor = React.memo(EditorUI); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx index d38e86df41464..c167155bd18a9 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -16,39 +16,70 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useEffect, useRef } from 'react'; import $ from 'jquery'; // @ts-ignore import { initializeOutput } from '../../../../../../../public/quarantined/src/output'; -import { useAppContext } from '../../../../context'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; +import { + useServicesContext, + useEditorReadContext, + useRequestReadContext, +} from '../../../../contexts'; + +// @ts-ignore +import utils from '../../../../../../../public/quarantined/src/utils'; + import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { applyCurrentSettings } from './apply_editor_settings'; -function _EditorOuput() { +function modeForContentType(contentType: string) { + if (contentType.indexOf('application/json') >= 0) { + return 'ace/mode/json'; + } else if (contentType.indexOf('application/yaml') >= 0) { + return 'ace/mode/yaml'; + } + return 'ace/mode/text'; +} + +function EditorOutputUI() { const editorRef = useRef(null); const editorInstanceRef = useRef(null); - const { - services: { settings }, - } = useAppContext(); - - const dispatch = useEditorActionContext(); + const { services } = useServicesContext(); const { settings: readOnlySettings } = useEditorReadContext(); + const { + lastResult: { data, error }, + } = useRequestReadContext(); useEffect(() => { const editor$ = $(editorRef.current!); - editorInstanceRef.current = initializeOutput(editor$, settings); - editorInstanceRef.current.update(''); + editorInstanceRef.current = initializeOutput(editor$, services.settings); const unsubscribe = subscribeResizeChecker(editorRef.current!, editorInstanceRef.current); - dispatch({ type: 'setOutputEditor', value: editorInstanceRef.current }); - return () => { unsubscribe(); }; - }, []); + }, [services.settings]); + + useEffect(() => { + if (data) { + const mode = modeForContentType(data[0].response.contentType); + editorInstanceRef.current.session.setMode(mode); + editorInstanceRef.current.update( + data + .map(d => d.response.value) + .map(readOnlySettings.tripleQuotes ? utils.expandLiteralStrings : a => a) + .join('\n') + ); + } else if (error) { + editorInstanceRef.current.session.setMode(modeForContentType(error.contentType)); + editorInstanceRef.current.update(error.value); + } else { + editorInstanceRef.current.update(''); + } + }, [readOnlySettings, data, error]); useEffect(() => { applyCurrentSettings(editorInstanceRef.current, readOnlySettings); @@ -61,4 +92,4 @@ function _EditorOuput() { ); } -export const EditorOutput = React.memo(_EditorOuput); +export const EditorOutput = React.memo(EditorOutputUI); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts index 134f3de42833b..832295d4eb00b 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts @@ -18,5 +18,4 @@ */ export { EditorOutput, Editor } from './console_editor'; -export { ConsoleHistory } from './console_history'; export { getDocumentation, autoIndent } from './console_menu_actions'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx index 269f4e2cdeb72..ca74b19b76f16 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx @@ -35,41 +35,40 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n const overlayMountNode = useRef(null); const autoCompleteVisibleRef = useRef(false); - function onDismissOverlay(event: KeyboardEvent) { - if (event.keyCode === keyCodes.ENTER) { - event.preventDefault(); - aceTextAreaElement!.focus(); - } - } - - function enableOverlay() { - if (overlayMountNode.current) { - overlayMountNode.current.focus(); + useEffect(() => { + function onDismissOverlay(event: KeyboardEvent) { + if (event.keyCode === keyCodes.ENTER) { + event.preventDefault(); + aceTextAreaElement!.focus(); + } } - } - const isAutoCompleteVisible = () => { - const autoCompleter = document.querySelector('.ace_autocomplete'); - if (!autoCompleter) { - return false; + function enableOverlay() { + if (overlayMountNode.current) { + overlayMountNode.current.focus(); + } } - // The autoComplete is just hidden when it's closed, not removed from the DOM. - return autoCompleter.style.display !== 'none'; - }; - const documentKeyDownListener = () => { - autoCompleteVisibleRef.current = isAutoCompleteVisible(); - }; + const isAutoCompleteVisible = () => { + const autoCompleter = document.querySelector('.ace_autocomplete'); + if (!autoCompleter) { + return false; + } + // The autoComplete is just hidden when it's closed, not removed from the DOM. + return autoCompleter.style.display !== 'none'; + }; - const aceKeydownListener = (event: KeyboardEvent) => { - if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { - event.preventDefault(); - event.stopPropagation(); - enableOverlay(); - } - }; + const documentKeyDownListener = () => { + autoCompleteVisibleRef.current = isAutoCompleteVisible(); + }; - useEffect(() => { + const aceKeydownListener = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { + event.preventDefault(); + event.stopPropagation(); + enableOverlay(); + } + }; if (aceTextAreaElement) { // We don't control HTML elements inside of ace so we imperatively create an element // that acts as a container and insert it just before ace's textarea element diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index 518630c5a07c1..764c4b8e87100 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -17,33 +17,25 @@ * under the License. */ -import React, { useCallback, useState } from 'react'; -import { debounce } from 'lodash'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; - -import { EditorOutput, Editor, ConsoleHistory } from '../editor'; +import { ConsoleHistory } from '../console_history'; +import { Editor } from '../editor'; import { Settings } from '../settings'; -// TODO: find out what this is: $(document.body).removeClass('fouc'); - -import { TopNavMenu, WelcomePanel, HelpPanel, PanelsContainer, Panel } from '../../components'; +import { TopNavMenu, WelcomePanel, HelpPanel } from '../../components'; -import { useAppContext } from '../../context'; -import { StorageKeys } from '../../../services'; +import { useServicesContext, useEditorReadContext } from '../../contexts'; import { getTopNavConfig } from './get_top_nav'; -import { useEditorReadContext } from '../editor'; - -const INITIAL_PANEL_WIDTH = 50; -const PANEL_MIN_WIDTH = '100px'; export function Main() { const { services: { storage }, - } = useAppContext(); + } = useServicesContext(); - const { editorsReady } = useEditorReadContext(); + const { ready: editorsReady } = useEditorReadContext(); const [showWelcome, setShowWelcomePanel] = useState( () => storage.get('version_welcome_shown') !== '@@SENSE_REVISION' @@ -53,18 +45,6 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ - INITIAL_PANEL_WIDTH, - INITIAL_PANEL_WIDTH, - ]); - - const onPanelWidthChange = useCallback( - debounce((widths: number[]) => { - storage.set(StorageKeys.WIDTH, widths); - }, 300), - [] - ); - const renderConsoleHistory = () => { return editorsReady ? setShowHistory(false)} /> : null; }; @@ -95,20 +75,7 @@ export function Main() { {showingHistory ? {renderConsoleHistory()} : null} - - - - - - - - + diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx index d794dc9302c25..8440faa6eeea8 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx @@ -22,9 +22,8 @@ import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; // @ts-ignore import mappings from '../../../../public/quarantined/src/mappings'; -import { useAppContext } from '../context'; +import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings } from '../../services'; -import { useEditorActionContext } from './editor/context'; const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToolsSettings) => { return Object.keys(newSettings.autocomplete).filter(key => { @@ -76,7 +75,7 @@ export interface Props { export function Settings({ onClose }: Props) { const { services: { settings }, - } = useAppContext(); + } = useServicesContext(); const dispatch = useEditorActionContext(); @@ -90,7 +89,7 @@ export function Settings({ onClose }: Props) { // Let the rest of the application know settings have updated. dispatch({ type: 'updateSettings', - value: newSettings, + payload: newSettings, }); onClose(); }; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts new file mode 100644 index 0000000000000..03b93c7d5b8ba --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts @@ -0,0 +1,30 @@ +/* + * 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 { Context, useContext } from 'react'; + +export const createUseContext = (Ctx: Context, name: string) => { + return () => { + const ctx = useContext(Ctx); + if (!ctx) { + throw new Error(`${name} should be used inside of ${name}Provider!`); + } + return ctx; + }; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx similarity index 51% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx index aa04a5ff3dd96..d5ed44e3f6ba2 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx @@ -17,52 +17,30 @@ * under the License. */ -import React, { createContext, Dispatch, useContext, useReducer } from 'react'; -import { Action, reducer } from './reducer'; -import { DevToolsSettings } from '../../../../services'; +import React, { createContext, Dispatch, useReducer } from 'react'; +import * as editor from '../../stores/editor'; +import { DevToolsSettings } from '../../../services'; +import { createUseContext } from '../create_use_context'; -export interface ContextValue { - editorsReady: boolean; - settings: DevToolsSettings; -} - -const EditorReadContext = createContext(null as any); -const EditorActionContext = createContext>(null as any); +const EditorReadContext = createContext(null as any); +const EditorActionContext = createContext>(null as any); export interface EditorContextArgs { children: any; settings: DevToolsSettings; } -const initialValue: ContextValue = { - editorsReady: false, - settings: null as any, -}; - export function EditorContextProvider({ children, settings }: EditorContextArgs) { - const [state, dispatch] = useReducer(reducer, initialValue, value => ({ + const [state, dispatch] = useReducer(editor.reducer, editor.initialValue, value => ({ ...value, settings, })); return ( - + {children} ); } -export const useEditorActionContext = () => { - const context = useContext(EditorActionContext); - if (context === undefined) { - throw new Error('useEditorActionContext must be used inside EditorActionContext'); - } - return context; -}; - -export const useEditorReadContext = () => { - const context = useContext(EditorReadContext); - if (context === undefined) { - throw new Error('useEditorReadContext must be used inside EditorContextProvider'); - } - return context; -}; +export const useEditorReadContext = createUseContext(EditorReadContext, 'EditorReadContext'); +export const useEditorActionContext = createUseContext(EditorActionContext, 'EditorActionContext'); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts similarity index 87% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts index 6f14c6fc84150..bdccc1af0860c 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts @@ -19,23 +19,14 @@ export class EditorRegistry { inputEditor: any; - outputEditor: any; setInputEditor(inputEditor: any) { this.inputEditor = inputEditor; } - setOutputEditor(outputEditor: any) { - this.outputEditor = outputEditor; - } - getInputEditor() { return this.inputEditor; } - - getOutputEditor() { - return this.outputEditor; - } } // Create a single instance of this and use as private state. diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/index.ts similarity index 100% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/index.ts diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts new file mode 100644 index 0000000000000..18234acf15957 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useServicesContext, ServicesContextProvider } from './services_context'; + +export { + useRequestActionContext, + useRequestReadContext, + RequestContextProvider, +} from './request_context'; + +export { + useEditorActionContext, + useEditorReadContext, + EditorContextProvider, +} from './editor_context'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx new file mode 100644 index 0000000000000..faaa3196a97bc --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx @@ -0,0 +1,40 @@ +/* + * 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, { createContext, useReducer, Dispatch } from 'react'; +import { createUseContext } from './create_use_context'; +import * as store from '../stores/request'; + +const RequestReadContext = createContext(null as any); +const RequestActionContext = createContext>(null as any); + +export function RequestContextProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(store.reducer, store.initialValue); + return ( + + {children} + + ); +} + +export const useRequestReadContext = createUseContext(RequestReadContext, 'RequestReadContext'); +export const useRequestActionContext = createUseContext( + RequestActionContext, + 'RequestActionContext' +); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx similarity index 77% rename from src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx index be7aa87ac2894..f715b1ae53a78 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx @@ -18,7 +18,7 @@ */ import React, { createContext, useContext } from 'react'; -import { NotificationsSetup } from '../../../../../../../core/public'; +import { NotificationsSetup } from 'kibana/public'; import { History, Storage, Settings } from '../../services'; interface ContextValue { @@ -36,14 +36,14 @@ interface ContextProps { children: any; } -const AppContext = createContext(null as any); +const ServicesContext = createContext(null as any); -export function AppContextProvider({ children, value }: ContextProps) { - return {children}; +export function ServicesContextProvider({ children, value }: ContextProps) { + return {children}; } -export const useAppContext = () => { - const context = useContext(AppContext); +export const useServicesContext = () => { + const context = useContext(ServicesContext); if (context === undefined) { throw new Error('useAppContext must be used inside the AppContextProvider.'); } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts new file mode 100644 index 0000000000000..8c5a8d599a0df --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useSetInputEditor } from './use_set_input_editor'; +export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts new file mode 100644 index 0000000000000..017344ae537ab --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts similarity index 90% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts index f7f691e083ca2..b053e605b5fae 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts @@ -17,6 +17,10 @@ * under the License. */ +/** + * This function is considered legacy and should not be changed or updated before we have editor + * interfaces in place (it's using a customized version of Ace directly). + */ export function restoreRequestFromHistory(input: any, req: any) { const session = input.getSession(); let pos = input.getCursorPosition(); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts new file mode 100644 index 0000000000000..590ad78e6c236 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts @@ -0,0 +1,28 @@ +/* + * 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 { useCallback } from 'react'; +import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { restoreRequestFromHistory } from './restore_request_from_history'; + +export const useRestoreRequestFromHistory = () => { + return useCallback((req: any) => { + const editor = registry.getInputEditor(); + restoreRequestFromHistory(editor, req); + }, []); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts similarity index 91% rename from src/legacy/core_plugins/console/np_ready/public/application/context/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts index 27d69f5736ffe..a8f59d573c1a0 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { useAppContext, AppContextProvider } from './app_context'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts similarity index 50% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index d3abf9c92f48e..22fa4477e9012 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -18,55 +18,49 @@ */ // @ts-ignore -import mappings from '../../../../../../../public/quarantined/src/mappings'; +import utils from '../../../../../public/quarantined/src/utils'; // @ts-ignore -import utils from '../../../../../../../public/quarantined/src/utils'; -// @ts-ignore -import * as es from '../../../../../../../public/quarantined/src/es'; +import * as es from '../../../../../public/quarantined/src/es'; +import { BaseResponseType } from '../../../types/common'; export interface EsRequestArgs { - callback: (esPath: any, esMethod: any, esData: any) => void; - input?: any; - output?: any; - isPolling: boolean; - isUsingTripleQuotes: boolean; + requests: any; +} + +export interface ESRequestResult { + request: { + path: string; + data: any; + method: string; + }; + response: { + contentType: BaseResponseType; + value: unknown; + }; } let CURRENT_REQ_ID = 0; -export function sendCurrentRequestToES({ - callback, - input, - output, - isPolling, - isUsingTripleQuotes, -}: EsRequestArgs) { - const reqId = ++CURRENT_REQ_ID; - - input.getRequestsInRange((requests: any) => { +export function sendRequestToES({ requests }: EsRequestArgs): Promise { + return new Promise((resolve, reject) => { + const reqId = ++CURRENT_REQ_ID; + const results: ESRequestResult[] = []; if (reqId !== CURRENT_REQ_ID) { return; } - if (output) { - output.update(''); - } if (requests.length === 0) { return; } const isMultiRequest = requests.length > 1; - const finishChain = () => { - /* noop */ - }; - - let isFirstRequest = true; const sendNextRequest = () => { if (reqId !== CURRENT_REQ_ID) { + resolve(results); return; } if (requests.length === 0) { - finishChain(); + resolve(results); return; } const req = requests.shift(); @@ -85,41 +79,13 @@ export function sendCurrentRequestToES({ const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; - function modeForContentType(contentType: string) { - if (contentType.indexOf('text/plain') >= 0) { - return 'ace/mode/text'; - } else if (contentType.indexOf('application/yaml') >= 0) { - return 'ace/mode/yaml'; - } - return null; - } - const isSuccess = typeof xhr.status === 'number' && // Things like DELETE index where the index is not there are OK. ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); if (isSuccess) { - if (xhr.status !== 404 && isPolling) { - // If the user has submitted a request against ES, something in the fields, indices, aliases, - // or templates may have changed, so we'll need to update this data. Assume that if - // the user disables polling they're trying to optimize performance or otherwise - // preserve resources, so they won't want this request sent either. - mappings.retrieveAutoCompleteInfo(); - } - let value = xhr.responseText; - const mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - - // Apply triple quotes to output. - if (isUsingTripleQuotes && mode === null) { - // assume json - auto pretty - try { - value = utils.expandLiteralStrings(value); - } catch (e) { - // nothing to do here - } - } const warnings = xhr.getResponseHeader('warning'); if (warnings) { @@ -131,47 +97,34 @@ export function sendCurrentRequestToES({ value = '# ' + req.method + ' ' + req.url + '\n' + value; } - if (output) { - if (isFirstRequest) { - output.update(value, mode); - } else { - output.append('\n' + value); - } - } + results.push({ + response: { + contentType: xhr.getResponseHeader('Content-Type'), + value, + }, + request: { + data: esData, + method: esMethod, + path: esPath, + }, + }); - isFirstRequest = false; // single request terminate via sendNextRequest as well - - callback(esPath, esMethod, esData); sendNextRequest(); } else { let value; - let mode; + let contentType: string; if (xhr.responseText) { value = xhr.responseText; // ES error should be shown - mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - if (value[0] === '{') { - try { - value = JSON.stringify(JSON.parse(value), null, 2); - } catch (e) { - // nothing to do here - } - } + contentType = xhr.getResponseHeader('Content-Type'); } else { value = 'Request failed to get to the server (status code: ' + xhr.status + ')'; - mode = 'ace/mode/text'; + contentType = 'text/plain'; } if (isMultiRequest) { value = '# ' + req.method + ' ' + req.url + '\n' + value; } - if (output) { - if (isFirstRequest) { - output.update(value, mode); - } else { - output.append('\n' + value); - } - } - finishChain(); + reject({ value, contentType }); } } ); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts new file mode 100644 index 0000000000000..63d1120808e02 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; +import { useCallback } from 'react'; +import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { useServicesContext } from '../../contexts'; +import { sendRequestToES } from './send_request_to_es'; +import { useRequestActionContext } from '../../contexts'; +// @ts-ignore +import mappings from '../../../../../public/quarantined/src/mappings'; + +export const useSendCurrentRequestToES = () => { + const { + services: { history, settings, notifications }, + } = useServicesContext(); + + const dispatch = useRequestActionContext(); + + return useCallback(async () => { + dispatch({ type: 'sendRequest', payload: undefined }); + try { + const editor = registry.getInputEditor(); + const requests = await new Promise(resolve => editor.getRequestsInRange(resolve)); + if (!requests.length) { + dispatch({ + type: 'requestFail', + payload: { value: 'No requests in range', contentType: 'text/plain' }, + }); + return; + } + const results = await sendRequestToES({ + requests, + }); + + results.forEach(({ request: { path, method, data } }) => { + history.addToHistory(path, method, data); + }); + + const { polling } = settings.toJSON(); + if (polling) { + // If the user has submitted a request against ES, something in the fields, indices, aliases, + // or templates may have changed, so we'll need to update this data. Assume that if + // the user disables polling they're trying to optimize performance or otherwise + // preserve resources, so they won't want this request sent either. + mappings.retrieveAutoCompleteInfo(); + } + + dispatch({ + type: 'requestSuccess', + payload: { + data: results, + }, + }); + } catch (e) { + if (e.contentType) { + dispatch({ + type: 'requestFail', + payload: e, + }); + } else { + notifications.toasts.addError(e, { + title: i18n.translate('console.unknownRequestErrorTitle', { + defaultMessage: 'Unknown Request Error', + }), + }); + } + } + }, [dispatch, settings, history, notifications]); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts new file mode 100644 index 0000000000000..672f3e269ead9 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts @@ -0,0 +1,30 @@ +/* + * 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 { useEditorActionContext } from '../contexts/editor_context'; +import { instance as registry } from '../contexts/editor_context/editor_registry'; + +export const useSetInputEditor = () => { + const dispatch = useEditorActionContext(); + + return (editor: any) => { + dispatch({ type: 'setInputEditor', payload: editor }); + registry.setInputEditor(editor); + }; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/index.tsx b/src/legacy/core_plugins/console/np_ready/public/application/index.tsx index aaacfd3894d18..e181caf23d2cb 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/index.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/index.tsx @@ -18,9 +18,8 @@ */ import React from 'react'; -import { NotificationsSetup } from '../../../../../../core/public'; -import { AppContextProvider } from './context'; -import { EditorContextProvider } from './containers/editor/context'; +import { NotificationsSetup } from 'src/core/public'; +import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings, Settings } from '../services'; @@ -46,16 +45,18 @@ export function boot(deps: { return ( - - -
- - + + +
+ + + ); } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts new file mode 100644 index 0000000000000..339a2f7a2c4af --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts @@ -0,0 +1,57 @@ +/* + * 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 { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; +import { DevToolsSettings } from '../../services'; + +export interface Store { + ready: boolean; + settings: DevToolsSettings; +} + +export const initialValue: Store = produce( + { + ready: false, + settings: null as any, + }, + identity +); + +export type Action = + | { type: 'setInputEditor'; payload: any } + | { type: 'updateSettings'; payload: DevToolsSettings }; + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'setInputEditor') { + if (action.payload) { + draft.ready = true; + } + return; + } + + if (action.type === 'updateSettings') { + draft.settings = action.payload; + return; + } + + return draft; + }); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts b/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts new file mode 100644 index 0000000000000..fec7f4195eb74 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts @@ -0,0 +1,71 @@ +/* + * 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 { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; +import { BaseResponseType } from '../../types/common'; +import { ESRequestResult } from '../hooks/use_send_current_request_to_es/send_request_to_es'; + +export type Actions = + | { type: 'sendRequest'; payload: undefined } + | { type: 'requestSuccess'; payload: { data: ESRequestResult[] } } + | { type: 'requestFail'; payload: { contentType: BaseResponseType; value: string } }; + +export interface Store { + requestInFlight: boolean; + lastResult: { + data: ESRequestResult[] | null; + error?: { contentType: BaseResponseType; value: string }; + }; +} + +const initialResultValue = { + data: null, + type: 'unknown' as BaseResponseType, +}; + +export const initialValue: Store = produce( + { + requestInFlight: false, + lastResult: initialResultValue, + }, + identity +); + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'sendRequest') { + draft.requestInFlight = true; + draft.lastResult = initialResultValue; + return; + } + + if (action.type === 'requestSuccess') { + draft.requestInFlight = false; + draft.lastResult = action.payload; + return; + } + + if (action.type === 'requestFail') { + draft.requestInFlight = false; + draft.lastResult = { ...initialResultValue, error: action.payload }; + return; + } + }); diff --git a/src/legacy/core_plugins/console/np_ready/public/types/common.ts b/src/legacy/core_plugins/console/np_ready/public/types/common.ts new file mode 100644 index 0000000000000..ad9ed10d4188f --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/types/common.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type BaseResponseType = + | 'application/json' + | 'text/csv' + | 'text/tab-separated-values' + | 'text/plain' + | 'application/yaml' + | 'unknown'; From 03dad2827ecad7ec2d5dd6591a3f451b092c4e5e Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Nov 2019 11:39:20 -0700 Subject: [PATCH 12/52] run mocha tests from x-pack with root mocha script (#51352) * run mocha tests from x-pack with root mocha script * Only run Karma tests in xpack intake job * disable failing suites * fix typo * skip correct suite (there are multiple root suites) * support disabling junit reporting with $DISABLE_JUNIT_REPORTER * don't generate junit in ispec_plugin tests --- .../integration_tests/generate_plugin.test.js | 14 +++- .../lib/config/schema.ts | 2 +- .../kbn-test/src/mocha/auto_junit_reporter.js | 2 +- packages/kbn-test/src/mocha/run_mocha_cli.js | 11 ++- src/dev/jest/junit_reporter.js | 2 +- tasks/config/karma.js | 20 ++++-- test/scripts/jenkins_xpack.sh | 2 +- x-pack/gulpfile.js | 3 +- .../__tests__/grokdebugger_request.js | 3 +- .../__tests__/get_all_stats.js | 3 +- .../__tests__/get_cluster_uuids.js | 3 +- .../__tests__/helpers/cancellation_token.js | 3 +- .../server/lib/esqueue/__tests__/worker.js | 4 +- .../lib/validate/__tests__/validate_config.js | 3 +- x-pack/package.json | 4 +- x-pack/tasks/test.ts | 27 +------- yarn.lock | 68 ------------------- 17 files changed, 59 insertions(+), 115 deletions(-) diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index b43dcd80b4462..aa6611f3b6738 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -70,11 +70,21 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug describe(`then running`, () => { it(`'yarn test:browser' should exit 0`, async () => { - await execa('yarn', ['test:browser'], { cwd: generatedPath }); + await execa('yarn', ['test:browser'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { cwd: generatedPath }); + await execa('yarn', ['test:server'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn build' should exit 0`, async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 52672d5f039fb..4530b61423620 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -143,7 +143,7 @@ export const schema = Joi.object() junit: Joi.object() .keys({ - enabled: Joi.boolean().default(!!process.env.CI), + enabled: Joi.boolean().default(!!process.env.CI && !process.env.DISABLE_JUNIT_REPORTER), reportName: Joi.string(), }) .default(), diff --git a/packages/kbn-test/src/mocha/auto_junit_reporter.js b/packages/kbn-test/src/mocha/auto_junit_reporter.js index 50b589fbc57a5..b6e79616e1cde 100644 --- a/packages/kbn-test/src/mocha/auto_junit_reporter.js +++ b/packages/kbn-test/src/mocha/auto_junit_reporter.js @@ -29,7 +29,7 @@ export function createAutoJUnitReporter(junitReportOptions) { new MochaSpecReporter(runner, options); // in CI we also setup the JUnit reporter - if (process.env.CI) { + if (process.env.CI && !process.env.DISABLE_JUNIT_REPORTER) { setupJUnitReportGeneration(runner, junitReportOptions); } } diff --git a/packages/kbn-test/src/mocha/run_mocha_cli.js b/packages/kbn-test/src/mocha/run_mocha_cli.js index 7a90108472721..77f40aded1d7f 100644 --- a/packages/kbn-test/src/mocha/run_mocha_cli.js +++ b/packages/kbn-test/src/mocha/run_mocha_cli.js @@ -63,7 +63,16 @@ export function runMochaCli() { if (!opts._.length) { globby .sync( - ['src/**/__tests__/**/*.js', 'packages/**/__tests__/**/*.js', 'tasks/**/__tests__/**/*.js'], + [ + 'src/**/__tests__/**/*.js', + 'packages/**/__tests__/**/*.js', + 'tasks/**/__tests__/**/*.js', + 'x-pack/common/**/__tests__/**/*.js', + 'x-pack/server/**/__tests__/**/*.js', + `x-pack/legacy/plugins/*/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/common/**/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/**/server/**/__tests__/**/*.js`, + ], { cwd: REPO_ROOT, onlyFiles: true, diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 30501965bf1e7..7f51326ee46bb 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -45,7 +45,7 @@ export default class JestJUnitReporter { * @return {undefined} */ onRunComplete(contexts, results) { - if (!process.env.CI || !results.testResults.length) { + if (!process.env.CI || process.env.DISABLE_JUNIT_REPORTER || !results.testResults.length) { return; } diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 16947a97a3d14..25723677390bd 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -34,6 +34,19 @@ module.exports = function (grunt) { return 'Chrome'; } + function pickReporters() { + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + if (process.env.CI && process.env.DISABLE_JUNIT_REPORTER) { + return ['dots']; + } + + if (process.env.CI) { + return ['dots', 'junit']; + } + + return ['progress']; + } + const config = { options: { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -63,14 +76,13 @@ module.exports = function (grunt) { }, }, - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: process.env.CI ? ['dots', 'junit'] : ['progress'], + reporters: pickReporters(), junitReporter: { outputFile: resolve(ROOT, 'target/junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}karma.xml`), useBrowserName: false, - nameFormatter: (browser, result) => [...result.suite, result.description].join(' '), - classNameFormatter: (browser, result) => { + nameFormatter: (_, result) => [...result.suite, result.description].join(' '), + classNameFormatter: (_, result) => { const rootSuite = result.suite[0] || result.description; return `Browser Unit Tests.${rootSuite.replace(/\./g, '·')}`; }, diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 1a7a1c973102c..27f73c0b6e20d 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -6,7 +6,7 @@ export TEST_BROWSER_HEADLESS=1 echo " -> Running mocha tests" cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack Mocha" yarn test +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:browser echo "" echo "" diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js index 74e24692f59f6..d3f93c29e3df8 100644 --- a/x-pack/gulpfile.js +++ b/x-pack/gulpfile.js @@ -8,7 +8,7 @@ require('../src/setup_node_env'); const { buildTask } = require('./tasks/build'); const { devTask } = require('./tasks/dev'); -const { testTask, testBrowserTask, testBrowserDevTask, testServerTask } = require('./tasks/test'); +const { testTask, testBrowserTask, testBrowserDevTask } = require('./tasks/test'); const { prepareTask } = require('./tasks/prepare'); // export the tasks that are runnable from the CLI @@ -17,7 +17,6 @@ module.exports = { dev: devTask, prepare: prepareTask, test: testTask, - testserver: testServerTask, testbrowser: testBrowserTask, 'testbrowser-dev': testBrowserDevTask, }; diff --git a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js index a87999873e40f..616aefaf73f62 100644 --- a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js +++ b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { GrokdebuggerRequest } from '../grokdebugger_request'; -describe('grokdebugger_request', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('grokdebugger_request', () => { describe('GrokdebuggerRequest', () => { const downstreamRequest = { diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js index c1425de20d146..7b300939bd470 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { addStackStats, getAllStats, handleAllStats } from '../get_all_stats'; -describe('get_all_stats', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_all_stats', () => { const size = 123; const start = 0; const end = 1; diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js index e3153670ac58f..a0072e52fc7f7 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids'; -describe('get_cluster_uuids', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_cluster_uuids', () => { const callWith = sinon.stub(); const size = 123; const server = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js index 195a0d4fdbec4..6d638e50af476 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { CancellationToken } from '../../../../../common/cancellation_token'; -describe('CancellationToken', function () { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('CancellationToken', function () { let cancellationToken; beforeEach(function () { cancellationToken = new CancellationToken(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 84549d0680ff3..b2e87482b73a1 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -26,6 +26,7 @@ const defaultWorkerOptions = { intervalErrorMultiplier: 10 }; + describe('Worker class', function () { // some of these tests might be a little slow, give them a little extra time this.timeout(10000); @@ -1068,7 +1069,8 @@ describe('Format Job Object', () => { }); }); -describe('Get Doc Path from ES Response', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function () { const responseMock = { _index: 'foo', diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js index 9a74ba63b8e31..8b5d6f4591ff5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { validateConfig } from '../validate_config'; -describe('Reporting: Validate config', () => { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('Reporting: Validate config', () => { const logger = { warning: sinon.spy(), }; diff --git a/x-pack/package.json b/x-pack/package.json index f84db22fe5c40..bc7b220bf81f5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -14,8 +14,7 @@ "test:browser:dev": "gulp testbrowser-dev", "test:browser": "gulp testbrowser", "test:jest": "node scripts/jest", - "test:mocha": "node scripts/mocha", - "test:server": "gulp testserver" + "test:mocha": "node scripts/mocha" }, "kibana": { "build": { @@ -133,7 +132,6 @@ "graphql-codegen-typescript-resolvers": "^0.18.2", "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", - "gulp-mocha": "^7.0.2", "hapi": "^17.5.3", "jest": "^24.9.0", "jest-cli": "^24.9.0", diff --git a/x-pack/tasks/test.ts b/x-pack/tasks/test.ts index d26683899ce3f..0767d7479724a 100644 --- a/x-pack/tasks/test.ts +++ b/x-pack/tasks/test.ts @@ -5,35 +5,12 @@ */ import pluginHelpers from '@kbn/plugin-helpers'; -import { createAutoJUnitReporter } from '@kbn/test'; -// @ts-ignore no types available -import mocha from 'gulp-mocha'; import gulp from 'gulp'; import { getEnabledPlugins } from './helpers/flags'; export const testServerTask = async () => { - const pluginIds = await getEnabledPlugins(); - - const testGlobs = ['common/**/__tests__/**/*.js', 'server/**/__tests__/**/*.js']; - - for (const pluginId of pluginIds) { - testGlobs.push( - `legacy/plugins/${pluginId}/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/common/**/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/**/server/**/__tests__/**/*.js` - ); - } - - return gulp.src(testGlobs, { read: false }).pipe( - mocha({ - ui: 'bdd', - require: require.resolve('../../src/setup_node_env'), - reporter: createAutoJUnitReporter({ - reportName: 'X-Pack Mocha Tests', - }), - }) - ); + throw new Error('server mocha tests are now included in the `node scripts/mocha` script'); }; export const testBrowserTask = async () => { @@ -51,4 +28,4 @@ export const testBrowserDevTask = async () => { }); }; -export const testTask = gulp.series(testServerTask, testBrowserTask); +export const testTask = gulp.series(testBrowserTask, testServerTask); diff --git a/yarn.lock b/yarn.lock index 3296fc013c48d..e30abf76145a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9482,11 +9482,6 @@ dargs@^5.1.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" integrity sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk= -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -11651,21 +11646,6 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e" - integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ== - dependencies: - cross-spawn "^6.0.5" - get-stream "^5.0.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^3.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" @@ -14225,18 +14205,6 @@ gulp-cli@^2.2.0: v8flags "^3.0.1" yargs "^7.1.0" -gulp-mocha@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.2.tgz#c7e13d133b3fde96d777e877f90b46225255e408" - integrity sha512-ZXBGN60TXYnFhttr19mfZBOtlHYGx9SvCSc+Kr/m2cMIGloUe176HBPwvPqlakPuQgeTGVRS47NmcdZUereKMQ== - dependencies: - dargs "^7.0.0" - execa "^2.0.4" - mocha "^6.2.0" - plugin-error "^1.0.1" - supports-color "^7.0.0" - through2 "^3.0.1" - gulp-rename@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd" @@ -19352,35 +19320,6 @@ mocha-junit-reporter@^1.23.1: strip-ansi "^4.0.0" xml "^1.0.0" -mocha@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c" - integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "2.2.0" - minimatch "3.0.4" - mkdirp "0.5.1" - ms "2.1.1" - node-environment-flags "1.0.5" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.0" - yargs-parser "13.1.1" - yargs-unparser "1.6.0" - mocha@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20" @@ -20193,13 +20132,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== - dependencies: - path-key "^3.0.0" - npm-run-path@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438" From adc11a5d61d4991f9eb85e895a2b89e6e5e68c9a Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Nov 2019 11:44:31 -0700 Subject: [PATCH 13/52] increase the timeout when checking for deprecation log (#51505) * increase the timeout when checking for deprecation log * re-enable the tests --- src/legacy/server/config/__tests__/deprecation_warnings.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index 3cebc730e66de..0915f7de25b45 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -25,8 +25,7 @@ const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startu const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); const SECOND = 1000; -// FLAKY: https://github.com/elastic/kibana/issues/51479 -describe.skip('config/deprecation warnings', function () { +describe('config/deprecation warnings', function () { this.timeout(15 * SECOND); let stdio = ''; @@ -52,9 +51,9 @@ describe.skip('config/deprecation warnings', function () { } }); - // Either time out in 10 seconds, or resolve once the line is in our buffer + // Either time out in 60 seconds, or resolve once the line is in our buffer return Promise.race([ - new Promise((resolve) => setTimeout(resolve, 10000)), + new Promise((resolve) => setTimeout(resolve, 60000)), new Promise((resolve, reject) => { proc.stdout.on('data', (chunk) => { stdio += chunk.toString('utf8'); From 3dcb94db943ba259f7f667f6d7d4e70a05107667 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 25 Nov 2019 13:39:49 -0600 Subject: [PATCH 14/52] [APM] Some miscellaneous client new platform updates (#51482) * Move `setHelpExtension` to plugin start method instead of plugin root * Move `setHelpExtension` to a separate file * Remove 'ui/modules' import * Use new platform capabilities in useUpdateBadgeEffect * Move useUpdateBadgeEffect to a utility function called in start * Add plugins and plugins context to new platform start * Use new platform plugins for KueryBar autocomplete provider * Add types for plugin and rename to ApmPublicPlugin * Add empty setup method to plugin * Move all context providers from App to render method * Remove some unnecessary mocks References #32894. --- .../public/components/app/Home/Home.test.tsx | 1 - .../app/Main/useUpdateBadgeEffect.ts | 31 ------ .../DatePicker/__test__/DatePicker.test.tsx | 2 - .../components/shared/KueryBar/index.tsx | 19 ++-- x-pack/legacy/plugins/apm/public/index.tsx | 35 +----- .../plugins/apm/public/new-platform/index.tsx | 10 +- .../apm/public/new-platform/plugin.tsx | 104 ++++++++++++------ .../public/new-platform/setHelpExtension.ts | 33 ++++++ .../apm/public/new-platform/updateBadge.ts | 27 +++++ 9 files changed, 153 insertions(+), 109 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts create mode 100644 x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts create mode 100644 x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx index 035015c82a0ac..7a23c9f7de842 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); describe('Home component', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts b/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts deleted file mode 100644 index bb9f581129c5e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { useEffect } from 'react'; -import { capabilities } from 'ui/capabilities'; -import { useKibanaCore } from '../../../../../observability/public'; - -export const useUpdateBadgeEffect = () => { - const { chrome } = useKibanaCore(); - - useEffect(() => { - const uiCapabilities = capabilities.get(); - chrome.setBadge( - !uiCapabilities.apm.save - ? { - text: i18n.translate('xpack.apm.header.badge.readOnly.text', { - defaultMessage: 'Read only' - }), - tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save' - }), - iconType: 'glasses' - } - : undefined - ); - }, [chrome]); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 881e5975fc81f..05094c59712a9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -18,8 +18,6 @@ import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; -jest.mock('ui/kfetch'); - const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); const MockUrlParamsProvider: React.FC<{ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 66946e5b447f9..24d320505c994 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; @@ -18,16 +17,17 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { + AutocompleteSuggestion, + AutocompleteProvider +} from '../../../../../../../../src/plugins/data/public'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; +import { usePlugins } from '../../../new-platform/plugin'; const Container = styled.div` margin-bottom: 10px; `; -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); - interface State { suggestions: AutocompleteSuggestion[]; isLoadingSuggestions: boolean; @@ -45,9 +45,9 @@ function getSuggestions( query: string, selectionStart: number, indexPattern: StaticIndexPattern, - boolFilter: unknown + boolFilter: unknown, + autocompleteProvider?: AutocompleteProvider ) { - const autocompleteProvider = getAutocompleteProvider('kuery'); if (!autocompleteProvider) { return []; } @@ -74,6 +74,8 @@ export function KueryBar() { }); const { urlParams } = useUrlParams(); const location = useLocation(); + const { data } = usePlugins(); + const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; @@ -108,7 +110,8 @@ export function KueryBar() { inputValue, selectionStart, indexPattern, - boolFilter + boolFilter, + autocompleteProvider ) ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx index 8fd3cb0893dea..db14e1c520020 100644 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ b/x-pack/legacy/plugins/apm/public/index.tsx @@ -6,43 +6,18 @@ import { npStart } from 'ui/new_platform'; import 'react-vis/dist/style.css'; +import { PluginInitializerContext } from 'kibana/public'; import 'ui/autoload/all'; import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import url from 'url'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; import { plugin } from './new-platform'; import { REACT_APP_ROOT_ID } from './new-platform/plugin'; import './style/global_overrides.css'; import template from './templates/index.html'; -const { core } = npStart; - -// render APM feedback link in global help menu -core.chrome.setHelpExtension({ - appName: i18n.translate('xpack.apm.feedbackMenu.appName', { - defaultMessage: 'APM' - }), - links: [ - { - linkType: 'discuss', - href: 'https://discuss.elastic.co/c/apm' - }, - { - linkType: 'custom', - href: url.format({ - pathname: core.http.basePath.prepend('/app/kibana'), - hash: '/management/elasticsearch/upgrade_assistant' - }), - content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { - defaultMessage: 'Upgrade assistant' - }) - } - ] -}); +const { core, plugins } = npStart; +// This will be moved to core.application.register when the new platform +// migration is complete. // @ts-ignore chrome.setRootTemplate(template); @@ -57,5 +32,5 @@ const checkForRoot = () => { }); }; checkForRoot().then(() => { - plugin().start(core); + plugin({} as PluginInitializerContext).start(core, plugins); }); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx index cb4cc2a845a4c..9dce4bcdd828c 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { PluginInitializer } from '../../../../../../src/core/public'; +import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; -export function plugin() { - return new Plugin(); -} +export const plugin: PluginInitializer< + ApmPluginSetup, + ApmPluginStart +> = _core => new ApmPlugin(); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index ac4aca4c795b7..b5986610d3048 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext, createContext } from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { LegacyCoreStart } from 'src/core/public'; +import { + CoreStart, + LegacyCoreStart, + Plugin, + CoreSetup +} from '../../../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { KibanaCoreContextProvider } from '../../../observability/public'; import { history } from '../utils/history'; import { LocationProvider } from '../context/LocationContext'; @@ -19,9 +25,10 @@ import { LicenseProvider } from '../context/LicenseContext'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { useUpdateBadgeEffect } from '../components/app/Main/useUpdateBadgeEffect'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; +import { setHelpExtension } from './setHelpExtension'; +import { setReadonlyBadge } from './updateBadge'; export const REACT_APP_ROOT_ID = 'react-apm-root'; @@ -31,41 +38,70 @@ const MainContainer = styled.main` `; const App = () => { - useUpdateBadgeEffect(); - return ( - - - - - - - - - {routes.map((route, i) => ( - - ))} - - - - - - + + + + + {routes.map((route, i) => ( + + ))} + + ); }; -export class Plugin { - public start(core: LegacyCoreStart) { - const { i18n } = core; +export type ApmPluginSetup = void; +export type ApmPluginStart = void; +export type ApmPluginSetupDeps = {}; // eslint-disable-line @typescript-eslint/consistent-type-definitions + +export interface ApmPluginStartDeps { + data: DataPublicPluginStart; +} + +const PluginsContext = createContext({} as ApmPluginStartDeps); + +export function usePlugins() { + return useContext(PluginsContext); +} + +export class ApmPlugin + implements + Plugin< + ApmPluginSetup, + ApmPluginStart, + ApmPluginSetupDeps, + ApmPluginStartDeps + > { + // Take the DOM element as the constructor, so we can mount the app. + public setup(_core: CoreSetup, _plugins: ApmPluginSetupDeps) {} + + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + const i18nCore = core.i18n; + + // render APM feedback link in global help menu + setHelpExtension(core); + setReadonlyBadge(core); + ReactDOM.render( - - - - - - - - + + + + + + + + + + + + + + + + + + , document.getElementById(REACT_APP_ROOT_ID) ); @@ -76,4 +112,6 @@ export class Plugin { console.log('Error fetching static index pattern', e); }); } + + public stop() {} } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts new file mode 100644 index 0000000000000..1a3394651b2ff --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import url from 'url'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setHelpExtension({ chrome, http }: CoreStart) { + chrome.setHelpExtension({ + appName: i18n.translate('xpack.apm.feedbackMenu.appName', { + defaultMessage: 'APM' + }), + links: [ + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/apm' + }, + { + linkType: 'custom', + href: url.format({ + pathname: http.basePath.prepend('/app/kibana'), + hash: '/management/elasticsearch/upgrade_assistant' + }), + content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { + defaultMessage: 'Upgrade assistant' + }) + } + ] + }); +} diff --git a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts new file mode 100644 index 0000000000000..b3e29bb891c23 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setReadonlyBadge({ application, chrome }: CoreStart) { + const canSave = application.capabilities.apm.save; + const { setBadge } = chrome; + + setBadge( + !canSave + ? { + text: i18n.translate('xpack.apm.header.badge.readOnly.text', { + defaultMessage: 'Read only' + }), + tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save' + }), + iconType: 'glasses' + } + : undefined + ); +} From 33e85370e311fd8797a75016127873cda21caf94 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Nov 2019 13:58:38 -0700 Subject: [PATCH 15/52] [Maps] refactor feature id assignment (#51317) * [Maps] refactor feature id assignment * add test case for falsy id value * review feedback --- .../maps/public/elasticsearch_geo_utils.js | 11 +-- .../public/elasticsearch_geo_utils.test.js | 6 +- .../client_file_source/geojson_file_source.js | 16 +--- .../ems_file_source/ems_file_source.js | 4 +- .../es_geo_grid_source/convert_to_geojson.js | 7 +- .../es_pew_pew_source/convert_to_lines.js | 6 +- .../kibana_regionmap_source.js | 5 +- .../public/layers/util/assign_feature_ids.js | 57 +++++++++++++ .../layers/util/assign_feature_ids.test.js | 83 +++++++++++++++++++ .../maps/public/layers/vector_layer.js | 46 ++-------- 10 files changed, 162 insertions(+), 79 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index ef2819f1f372c..4b04251edd94a 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -11,7 +11,6 @@ import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS, - FEATURE_ID_PROPERTY_NAME, GEO_JSON_TYPE, POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, @@ -81,12 +80,10 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { features.push({ type: 'Feature', geometry: tmpGeometriesAccumulator[j], - properties: { - ...properties, - // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions - // Need to prefix with _index to guarantee uniqueness - [FEATURE_ID_PROPERTY_NAME]: `${properties._index}:${properties._id}:${j}` - } + // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions + // Need to prefix with _index to guarantee uniqueness + id: `${properties._index}:${properties._id}:${j}`, + properties, }); } } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index 0b84b4c32f4ac..45aa2af15eb9d 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -74,8 +74,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', }, @@ -139,8 +139,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', myField: 8 @@ -152,8 +152,8 @@ describe('hitsToGeoJson', () => { coordinates: [110, 30], type: 'Point', }, + id: 'index1:doc1:1', properties: { - __kbn__feature_id__: 'index1:doc1:1', _id: 'doc1', _index: 'index1', myField: 8 diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 11a02d58a9198..920253d15eaee 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -10,7 +10,6 @@ import { ES_GEO_FIELD_TYPE, GEOJSON_FILE, ES_SIZE_LIMIT, - FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; import { ESSearchSource } from '../es_search_source'; @@ -137,21 +136,8 @@ export class GeojsonFileSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const copiedPropsFeatures = this._descriptor.__featureCollection.features.map((feature, index) => { - const properties = feature.properties ? { ...feature.properties } : {}; - properties[FEATURE_ID_PROPERTY_NAME] = index; - return { - type: 'Feature', - geometry: feature.geometry, - properties, - }; - }); - return { - data: { - type: 'FeatureCollection', - features: copiedPropsFeatures - }, + data: this._descriptor.__featureCollection, meta: {} }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index b2e04f56e5718..fcd52683b70ff 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -7,7 +7,7 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; -import { EMS_FILE, FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { EMS_FILE, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; @@ -94,7 +94,7 @@ export class EMSFileSource extends AbstractVectorSource { return field.type === 'id'; }); featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = emsIdField + feature.id = emsIdField ? feature.properties[emsIdField.id] : index; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js index c83f12ce992ff..d26bfd8bbeacb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js @@ -6,7 +6,7 @@ import { RENDER_AS } from './render_as'; import { getTileBoundingBox } from './geo_tile_utils'; -import { EMPTY_FEATURE_COLLECTION, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { EMPTY_FEATURE_COLLECTION } from '../../../../common/constants'; export function convertToGeoJson({ table, renderAs }) { @@ -34,9 +34,7 @@ export function convertToGeoJson({ table, renderAs }) { return; } - const properties = { - [FEATURE_ID_PROPERTY_NAME]: gridKey - }; + const properties = {}; metricColumns.forEach(metricColumn => { properties[metricColumn.aggConfig.id] = row[metricColumn.id]; }); @@ -49,6 +47,7 @@ export function convertToGeoJson({ table, renderAs }) { geocentroidColumn, renderAs, }), + id: gridKey, properties }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js index c334776e6c4e8..ae9435dc42c69 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js @@ -6,8 +6,6 @@ import _ from 'lodash'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; - const LAT_INDEX = 0; const LON_INDEX = 1; @@ -47,10 +45,10 @@ export function convertToLines(esResponse) { type: 'LineString', coordinates: [[sourceCentroid.location.lon, sourceCentroid.location.lat], dest] }, + id: `${dest.join()},${key}`, properties: { - [FEATURE_ID_PROPERTY_NAME]: `${dest.join()},${key}`, ...rest - } + }, }); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index ffccb18a69192..e29887edcf7d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -10,7 +10,7 @@ import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { FIELD_ORIGIN } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; export class KibanaRegionmapSource extends AbstractVectorSource { @@ -91,9 +91,6 @@ export class KibanaRegionmapSource extends AbstractVectorSource { featureCollectionPath: vectorFileMeta.meta.feature_collection_path, fetchUrl: vectorFileMeta.url }); - featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = index; - }); return { data: featureCollection }; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js new file mode 100644 index 0000000000000..2c0d08f86cfc0 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +let idCounter = 0; + +function generateNumericalId() { + const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; + idCounter = newId + 1; + return newId; +} + +export function assignFeatureIds(featureCollection) { + + // wrt https://github.com/elastic/kibana/issues/39317 + // In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. + // This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. + // This is a work-around to avoid hitting such a worst-case + // This was tested as a suitable work-around for mapbox-gl 0.54 + // The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 + + // This only shuffles the id-assignment, _not_ the features in the collection + // The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. + const ids = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const id = generateNumericalId(); + ids.push(id); + } + + const randomizedIds = _.shuffle(ids); + const features = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const numericId = randomizedIds[i]; + const feature = featureCollection.features[i]; + features.push({ + type: 'Feature', + geometry: feature.geometry, // do not copy geometry, this object can be massive + properties: { + // preserve feature id provided by source so features can be referenced across fetches + [FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id, + // create new object for properties so original is not polluted with kibana internal props + ...feature.properties, + }, + id: numericId, // Mapbox feature state id, must be integer + }); + } + + return { + type: 'FeatureCollection', + features + }; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js new file mode 100644 index 0000000000000..0678070f568a2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js @@ -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 { assignFeatureIds } from './assign_feature_ids'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +const featureId = 'myFeature1'; + +test('should provide unique id when feature.id is not provided', () => { + const featureCollection = { + features: [ + { + properties: {} + }, + { + properties: {} + }, + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + const feature2 = updatedFeatureCollection.features[1]; + expect(typeof feature1.id).toBe('number'); + expect(typeof feature2.id).toBe('number'); + expect(feature1.id).toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).not.toBe(feature2.id); +}); + +test('should preserve feature id when provided', () => { + const featureCollection = { + features: [ + { + id: featureId, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); +}); + +test('should preserve feature id for falsy value', () => { + const featureCollection = { + features: [ + { + id: 0, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(0); +}); + +test('should not modify original feature properties', () => { + const featureProperties = {}; + const featureCollection = { + features: [ + { + id: featureId, + properties: featureProperties + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); + expect(featureProperties).not.toHaveProperty(FEATURE_ID_PROPERTY_NAME); +}); + diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 362c7bfd72540..e6b07b983d898 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -23,6 +23,7 @@ import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; +import { assignFeatureIds } from './util/assign_feature_ids'; const VISIBILITY_FILTER_CLAUSE = ['all', [ @@ -59,16 +60,6 @@ const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ] ]; - -let idCounter = 0; - -function generateNumericalId() { - const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; - idCounter = newId + 1; - return newId; -} - - export class VectorLayer extends AbstractLayer { static type = LAYER_TYPE.VECTOR; @@ -478,15 +469,15 @@ export class VectorLayer extends AbstractLayer { try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); const layerName = await this.getDisplayName(); - const { data: featureCollection, meta } = + const { data: sourceFeatureCollection, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback.bind(null, requestToken) ); - this._assignIdsToFeatures(featureCollection); - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, featureCollection, meta); + const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, layerFeatureCollection, meta); return { refreshed: true, - featureCollection: featureCollection + featureCollection: layerFeatureCollection }; } catch (error) { if (!(error instanceof DataRequestAbortError)) { @@ -498,31 +489,6 @@ export class VectorLayer extends AbstractLayer { } } - _assignIdsToFeatures(featureCollection) { - - //wrt https://github.com/elastic/kibana/issues/39317 - //In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. - //This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. - //This is a work-around to avoid hitting such a worst-case - //This was tested as a suitable work-around for mapbox-gl 0.54 - //The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 - - //This only shuffles the id-assignment, _not_ the features in the collection - //The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. - const ids = []; - for (let i = 0; i < featureCollection.features.length; i++) { - const id = generateNumericalId(); - ids.push(id); - } - - const randomizedIds = _.shuffle(ids); - for (let i = 0; i < featureCollection.features.length; i++) { - const id = randomizedIds[i]; - const feature = featureCollection.features[i]; - feature.id = id; // Mapbox feature state id, must be integer - } - } - async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; @@ -534,8 +500,8 @@ export class VectorLayer extends AbstractLayer { } const joinStates = await this._syncJoins(syncContext); - await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); + await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } _getSourceFeatureCollection() { From 24df2a3716c5be2b560d14e96c46eafdb49f8929 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Nov 2019 14:54:28 -0700 Subject: [PATCH 16/52] add serverMocha config for flaky job (#51508) * add serverMocha config for flaky job * fix typo * no reason to setup everything over and over, just call scripts/mocha * force CI_GROUP param for testing * define local CI_GROUP_PARAM that can be assigned alternate values temporarily * add additional metadata to job description * add workerNumber param to worker block * use kibanaPipeline.getPostBuildWorker to define wrapper function * use kibanaPipeline specific bash function * revert changes made for debugging --- .ci/Jenkinsfile_flaky | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 8ad02b7162b6a..669395564db44 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,10 +3,13 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -// Looks like 'oss:ciGroup:1' or 'oss:firefoxSmoke' -def JOB_PARTS = params.CI_GROUP.split(':') +def CI_GROUP_PARAM = params.CI_GROUP + +// Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke', or 'all:serverMocha' +def JOB_PARTS = CI_GROUP_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' def JOB = JOB_PARTS[1] +def NEED_BUILD = JOB != 'serverMocha' def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) @@ -31,13 +34,15 @@ stage("Kibana Pipeline") { print "Agent ${agentNumberInside} - ${agentExecutions} executions" kibanaPipeline.withWorkers('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } - } else { - kibanaPipeline.buildXpack() } }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() } @@ -61,7 +66,17 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { - if (job == 'firefoxSmoke') { + if (job == 'serverMocha') { + return kibanaPipeline.getPostBuildWorker('serverMocha', { + kibanaPipeline.bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/mocha + """, + "run `node scripts/mocha`" + ) + }) + } else if (job == 'firefoxSmoke') { return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) From edf2751480f0edad82df4dde992fbf24a2ce20cb Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 25 Nov 2019 15:20:26 -0700 Subject: [PATCH 17/52] [SIEM][Detection Engine] Makes input and output indexes optional for the REST API (#51551) * Initial logic added, just need to change the input and output parameters to be optional now * Removed the input and output indexes as they are now optional in all the examples * Added a check for attributes and unit tests for the get_input_output * Removed more output_signals stuff from scripts * Updated the README docs * Removed input and output index for the conversion script * Removed the scripts doing a replacement of the output index so we can rely on defaults more and to make things simplier * Added ability to convert everything to an ndjson for importer, flipped enabled to false by default with the script --- .../convert_saved_search_to_signals.js | 21 +- .../plugins/siem/server/kibana.index.ts | 3 +- .../server/lib/detection_engine/README.md | 7 + .../alerts/get_input_output_index.test.ts | 286 ++++++++++++++++++ .../alerts/get_input_output_index.ts | 72 +++++ .../alerts/signals_alert_type.ts | 31 +- .../detection_engine/routes/schemas.test.ts | 8 +- .../lib/detection_engine/routes/schemas.ts | 4 +- .../detection_engine/scripts/post_signal.sh | 3 +- .../scripts/post_x_signals.sh | 1 - .../scripts/signals/root_or_admin_1.json | 2 - .../scripts/signals/root_or_admin_10.json | 2 - .../scripts/signals/root_or_admin_2.json | 2 - .../scripts/signals/root_or_admin_3.json | 2 - .../scripts/signals/root_or_admin_4.json | 2 - .../scripts/signals/root_or_admin_5.json | 2 - .../scripts/signals/root_or_admin_6.json | 2 - .../scripts/signals/root_or_admin_7.json | 2 - .../scripts/signals/root_or_admin_8.json | 2 - .../scripts/signals/root_or_admin_9.json | 2 - .../signals/root_or_admin_filter_9998.json | 2 - .../signals/root_or_admin_filter_9999.json | 2 - .../scripts/signals/root_or_admin_meta.json | 2 - .../signals/root_or_admin_saved_query_1.json | 2 - .../signals/root_or_admin_saved_query_2.json | 2 - .../signals/root_or_admin_saved_query_3.json | 2 - .../signals/root_or_admin_update_1.json | 2 - .../signals/root_or_admin_update_2.json | 2 - .../scripts/signals/watch_longmont.json | 2 - .../detection_engine/scripts/update_signal.sh | 3 +- 30 files changed, 416 insertions(+), 61 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js index 597d93a44210b..263a2a59de31f 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js @@ -34,9 +34,17 @@ const TYPE = 'query'; const FROM = 'now-6m'; const TO = 'now'; const IMMUTABLE = true; -const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; -const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; const RISK_SCORE = 50; +const ENABLED = false; +let allSignals = ''; +const allSignalsNdJson = 'all_rules.ndjson'; + +// For converting, if you want to use these instead of rely on the defaults then +// comment these in and use them for the script. Otherwise this is commented out +// so we can utilize the defaults of input and output which are based on saved objects +// of siem:defaultIndex and siem:defaultSignalsIndex +// const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; +// const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; const walk = dir => { const list = fs.readdirSync(dir); @@ -124,7 +132,6 @@ async function main() { risk_score: RISK_SCORE, description: description || title, immutable: IMMUTABLE, - index: INDEX, interval: INTERVAL, name: title, severity: SEVERITY, @@ -134,16 +141,22 @@ async function main() { query, language, filters: filter, - output_index: OUTPUT_INDEX, + enabled: ENABLED, + // comment these in if you want to use these for input output, otherwise + // with these two commented out, we will use the default saved objects from spaces. + // index: INDEX, + // output_index: OUTPUT_INDEX, }; fs.writeFileSync( `${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2) ); + allSignals += `${JSON.stringify(outputMessage)}\n`; } } ); + fs.writeFileSync(`${outputDir}/${allSignalsNdJson}`, allSignals); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index c79b2651c11cb..a92bca064dab9 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -32,7 +32,8 @@ export const initServerWithKibana = ( mode: EnvironmentMode ) => { if (kbnServer.plugins.alerting != null) { - const type = signalsAlertType({ logger }); + const version = kbnServer.config().get('pkg.version'); + const type = signalsAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 0a0439a9ace1b..5d9d87a1cbc2f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -104,6 +104,13 @@ You should also see the SIEM detect the feature flags and start the API endpoint server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints ``` +Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same +value as you did with the environment variable of SIGNALS_INDEX, which should be `.siem-signals-${your user id}` + +``` +.siem-signals-${your user id} +``` + Open a terminal and go into the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: ```sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts new file mode 100644 index 0000000000000..07eb7c885b443 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts @@ -0,0 +1,286 @@ +/* + * 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 { savedObjectsClientMock } from 'src/core/server/mocks'; +import { + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { getInputOutputIndex, getOutputIndex, getInputIndex } from './get_input_output_index'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; + +describe('get_input_output_index', () => { + let savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + let servicesMock: AlertServices = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + servicesMock = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + }); + + describe('getInputOutputIndex', () => { + test('Returns inputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns inputIndex as is if inputIndex is defined but outputIndex is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + null + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex is null but outputIndex is defined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + null, + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns a saved object outputIndex if both passed in are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex if passed in outputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are missing', async () => { + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object inputIndex if passed in inputIndex and outputIndex are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + 'output-index-1' + ); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes is missing', async () => { + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); + + describe('getOutputIndex', () => { + test('test output index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex('output-index-1', mockConfiguration); + expect(outputIndex).toEqual('output-index-1'); + }); + + test('configured output index is returned when output index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.siem-test-signals', + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual('.siem-test-signals'); + }); + + test('output index from constants is returned when output index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + }); + + describe('getInputIndex', () => { + test('test input index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(['input-index-1'], mockConfiguration); + expect(inputIndex).toEqual(['input-index-1']); + }); + + test('configured input index is returned when input index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['input-index-1', 'input-index-2'], + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(['input-index-1', 'input-index-2']); + }); + + test('input index from constants is returned when input index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts new file mode 100644 index 0000000000000..567ab27976d8d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts @@ -0,0 +1,72 @@ +/* + * 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 { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; + +interface IndexObjectAttributes extends SavedObjectAttributes { + [DEFAULT_INDEX_KEY]: string[]; + [DEFAULT_SIGNALS_INDEX_KEY]: string; +} + +export const getInputIndex = ( + inputIndex: string[] | undefined | null, + configuration: SavedObject +): string[] => { + if (inputIndex != null) { + return inputIndex; + } else { + if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { + return configuration.attributes[DEFAULT_INDEX_KEY]; + } else { + return defaultIndexPattern; + } + } +}; + +export const getOutputIndex = ( + outputIndex: string | undefined | null, + configuration: SavedObject +): string => { + if (outputIndex != null) { + return outputIndex; + } else { + if ( + configuration.attributes != null && + configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY] != null + ) { + return configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY]; + } else { + return DEFAULT_SIGNALS_INDEX; + } + } +}; + +export const getInputOutputIndex = async ( + services: AlertServices, + version: string, + inputIndex: string[] | null | undefined, + outputIndex: string | null | undefined +): Promise<{ + inputIndex: string[]; + outputIndex: string; +}> => { + if (inputIndex != null && outputIndex != null) { + return { inputIndex, outputIndex }; + } else { + const configuration = await services.savedObjectsClient.get('config', version); + return { + inputIndex: getInputIndex(inputIndex, configuration), + outputIndex: getOutputIndex(outputIndex, configuration), + }; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index 8308bca68e9af..dfc779329d3b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -12,8 +12,15 @@ import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkIndex } from './utils'; import { SignalAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; +import { getInputOutputIndex } from './get_input_output_index'; -export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTypeDefinition => { +export const signalsAlertType = ({ + logger, + version, +}: { + logger: Logger; + version: string; +}): SignalAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', @@ -26,9 +33,9 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp filter: schema.nullable(schema.object({}, { allowUnknowns: true })), ruleId: schema.string(), immutable: schema.boolean({ defaultValue: false }), - index: schema.arrayOf(schema.string()), + index: schema.nullable(schema.arrayOf(schema.string())), language: schema.nullable(schema.string()), - outputIndex: schema.string(), + outputIndex: schema.nullable(schema.string()), savedId: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), @@ -70,6 +77,12 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const searchAfterSize = size ? size : 1000; + const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( + services, + version, + index, + outputIndex + ); const esFilter = await getFilter({ type, filter, @@ -78,11 +91,11 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp query, savedId, services, - index, + index: inputIndex, }); const noReIndex = buildEventsSearchQuery({ - index, + index: inputIndex, from, to, filter: esFilter, @@ -98,7 +111,11 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const noReIndexResult = await services.callCluster('search', noReIndex); if (noReIndexResult.hits.total.value !== 0) { logger.info( - `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}": ${noReIndexResult.hits.total.value}` + `Found ${ + noReIndexResult.hits.total.value + } signals from the indexes of "${inputIndex.join( + ', ' + )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${signalsIndex}` ); } @@ -108,7 +125,7 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp services, logger, id: alertId, - signalsIndex: outputIndex, + signalsIndex, name, createdBy, updatedBy, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 5e5f37ca8a080..6639dc6a3dfd6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -174,7 +174,7 @@ describe('schemas', () => { ).toBeTruthy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does not validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( createSignalsSchema.validate>({ rule_id: 'rule-1', @@ -190,7 +190,7 @@ describe('schemas', () => { query: 'some query', language: 'kuery', }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { @@ -213,7 +213,7 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does not validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( createSignalsSchema.validate>({ rule_id: 'rule-1', @@ -228,7 +228,7 @@ describe('schemas', () => { filter: {}, risk_score: 50, }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index fa773b684eb5d..210ce5ca9fdce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -68,7 +68,7 @@ export const createSignalsSchema = Joi.object({ from: from.required(), rule_id, immutable: immutable.default(false), - index: index.required(), + index, interval: interval.default('5m'), query: Joi.when('type', { is: 'query', @@ -95,7 +95,7 @@ export const createSignalsSchema = Joi.object({ otherwise: Joi.forbidden(), }), }), - output_index: output_index.required(), + output_index, saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh index 8455e7d27ad47..b8bd0e0e0361f 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh @@ -18,13 +18,12 @@ SIGNALS=(${@:-./signals/root_or_admin_1.json}) for SIGNAL in "${SIGNALS[@]}" do { [ -e "$SIGNAL" ] || continue - POST=$(jq '.output_index=env.SIGNALS_INDEX' $SIGNAL) curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d "$POST" \ + -d @${SIGNAL} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh index 8362c576ff554..abb2111a91c1b 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh @@ -24,7 +24,6 @@ do { --data "{ \"rule_id\": \"${i}\", \"risk_score\": \"50\", - \"output_index\": \"${SIGNALS_INDEX}"\", \"description\": \"Detecting root and admin users\", \"index\": [\"auditbeat-*\", \"filebeat-*\", \"packetbeat-*\", \"winlogbeat-*\"], \"interval\": \"24h\", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json index 8586b29c29886..b00a5929d9ef1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json @@ -2,10 +2,8 @@ "rule_id": "rule-1", "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json index 85bc09f0f9f85..657439104e306 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json @@ -1,13 +1,11 @@ { "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "references": ["http://www.example.com", "https://ww.example.com"] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json index 8f2d826ae9ae1..137cf7eedbccf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json @@ -2,9 +2,7 @@ "rule_id": "rule-2", "risk_score": 2, "description": "Detecting root and admin users over a long period of time", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", - "output_index": ".siem-signals", "name": "Detect Root/Admin Users over a long period of time", "severity": "high", "type": "query", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json index 10bfc2e0d74a3..b9160c95621ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json @@ -2,10 +2,8 @@ "rule_id": "rule-3", "risk_score": 3, "description": "Detecting root and admin users as an empty set", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-16y", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json index 18cfb808007b3..364e7f00c9571 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json @@ -2,10 +2,8 @@ "rule_id": "rule-4", "risk_score": 4, "description": "Detecting root and admin users with lucene", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json index a445a839a8228..eb7f2ae03b64b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json @@ -2,10 +2,8 @@ "rule_id": "rule-5", "risk_score": 5, "description": "Detecting root and admin users over 24 hours on windows", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json index 6e2f7a3f82a50..94f30bc9f92df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json @@ -2,10 +2,8 @@ "rule_id": "rule-6", "risk_score": 6, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json index 9da8a11861a4d..81ec19a4fd0ef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json @@ -2,14 +2,12 @@ "rule_id": "rule-7", "risk_score": 7, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "lucene", "filters": [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json index ad8c651bb3ec8..de24263c6af5c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json @@ -2,14 +2,12 @@ "rule_id": "rule-8", "risk_score": 8, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json index 3658e6e4e9428..9bf2b1abf5f90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json @@ -2,14 +2,12 @@ "rule_id": "rule-9", "risk_score": 9, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json index db53ea07fe34b..2381e9e259c07 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json @@ -2,12 +2,10 @@ "rule_id": "rule-9999", "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "filter", - "output_index": ".siem-signals", "from": "now-6m", "to": "now", "filter": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json index e6cc661af404c..ee8fe1fc93fb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json @@ -2,12 +2,10 @@ "rule_id": "rule-9999", "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "filter", - "output_index": ".siem-signals", "from": "now-6m", "to": "now", "filter": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json index 266ceeba15d47..ed8f2e5745bea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json @@ -2,10 +2,8 @@ "rule_id": "rule-meta-data", "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json index d5559ebe23bdb..721644acd989d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-1", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json index e272273d817d2..b733b6bb8c592 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-2", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json index 9fc2c32c7daf1..df1b37f19bf29 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-3", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json index 42834141a72fd..09ddfb1c34a92 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json @@ -2,12 +2,10 @@ "rule_id": "rule-1", "risk_score": 98, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", "type": "query", - "output_index": ".siem-signals", "from": "now-6m", "to": "now-5m", "query": "user.name: root", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json index 4c03f041e6e2f..8a3c765519ef3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json @@ -2,7 +2,6 @@ "rule_id": "rule-1", "risk_score": 78, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", @@ -12,7 +11,6 @@ "immutable": true, "tags": ["some other tag for you"], "to": "now-5m", - "output_index": ".siem-signals", "query": "user.name: root", "language": "kuery", "references": ["https://update1.example.com", "https://update2.example.com"] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json index cfb5fab8b8493..a43398bd6876a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json @@ -2,12 +2,10 @@ "rule_id": "rule-longmont", "risk_score": 5, "description": "Detect Longmont activity", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", "name": "Detect Longmont activity", "severity": "high", "type": "query", - "output_index": ".siem-signals", "from": "now-1y", "to": "now", "query": "user.name: root or user.name: admin", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh index 6984e7b4c810b..04541e1df1fa1 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh @@ -18,13 +18,12 @@ SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) for SIGNAL in "${SIGNALS[@]}" do { [ -e "$SIGNAL" ] || continue - POST=$(jq '.output_index=env.SIGNALS_INDEX' $SIGNAL) curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d "$POST" \ + -d @${SIGNAL} \ | jq .; } & done From fff493e54ecd063c738509f3e41099c225d469d7 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 25 Nov 2019 16:29:35 -0600 Subject: [PATCH 18/52] Build bundles for all new platform plugins (#51525) --- ...ibana-plugin-server.pluginsservicesetup.md | 3 +- ...ver.pluginsservicesetup.uipluginconfigs.md | 11 --- ...in-server.pluginsservicesetup.uiplugins.md | 3 +- src/core/server/legacy/legacy_service.test.ts | 6 +- src/core/server/legacy/legacy_service.ts | 1 - .../server/plugins/plugins_service.mock.ts | 4 +- .../server/plugins/plugins_service.test.ts | 78 ++++++++++++++----- src/core/server/plugins/plugins_service.ts | 35 ++++++--- .../server/plugins/plugins_system.test.ts | 13 +--- src/core/server/plugins/plugins_system.ts | 21 +---- src/core/server/plugins/types.ts | 9 +-- src/core/server/server.api.md | 7 +- src/legacy/server/kbn_server.d.ts | 1 - src/legacy/ui/ui_render/ui_render_mixin.js | 2 +- src/optimize/base_optimizer.js | 8 +- src/optimize/index.js | 2 +- src/optimize/watch/optmzr_role.js | 2 +- 17 files changed, 113 insertions(+), 93 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md index 36d803ddea618..248726e26f393 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -16,6 +16,5 @@ export interface PluginsServiceSetup | Property | Type | Description | | --- | --- | --- | | [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | -| [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) | Map<PluginName, Observable<unknown>> | | -| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
} | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
internal: Map<PluginName, InternalPluginInfo>;
public: Map<PluginName, DiscoveredPlugin>;
browserConfigs: Map<PluginName, Observable<unknown>>;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md deleted file mode 100644 index 4bd57b873043e..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) - -## PluginsServiceSetup.uiPluginConfigs property - -Signature: - -```typescript -uiPluginConfigs: Map>; -``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md index fa286dfb59092..7c47304cb9bf6 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -8,7 +8,8 @@ ```typescript uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; ``` diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 1240518422e2f..030caa8324521 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -41,7 +41,7 @@ import { configServiceMock } from '../config/config_service.mock'; import { BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; +import { DiscoveredPlugin } from '../plugins'; import { KibanaMigrator } from '../saved_objects/migrations'; import { ISavedObjectsClientProvider } from '../saved_objects'; @@ -84,9 +84,9 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + internal: new Map([['plugin-id', { entryPointPath: 'path/to/plugin/public' }]]), + browserConfigs: new Map(), }, - uiPluginConfigs: new Map(), }, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index e86e6cde6e927..99963ad9ce3e8 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -278,7 +278,6 @@ export class LegacyService implements CoreService { hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, - uiPluginConfigs: setupDeps.core.plugins.uiPluginConfigs, elasticsearch: setupDeps.core.elasticsearch, uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index e3be8fbb98309..8d3c6a8c909a2 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -30,10 +30,10 @@ const createServiceMock = () => { mocked.setup.mockResolvedValue({ contracts: new Map(), uiPlugins: { - public: new Map(), + browserConfigs: new Map(), internal: new Map(), + public: new Map(), }, - uiPluginConfigs: new Map(), }); mocked.start.mockResolvedValue({ contracts: new Map() }); return mocked; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index da6d1d5a010e7..7e55faa43360e 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,11 +33,12 @@ import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; import { take } from 'rxjs/operators'; -import { DiscoveredPluginInternal } from './types'; +import { DiscoveredPlugin } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; +let config$: BehaviorSubject; let configService: ConfigService; let coreId: symbol; let env: Env; @@ -107,11 +108,10 @@ describe('PluginsService', () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - configService = new ConfigService( - new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })), - env, - logger + config$ = new BehaviorSubject( + new ObjectToConfigAdapter({ plugins: { initialize: true } }) ); + configService = new ConfigService(config$, env, logger); await configService.setSchema(config.path, config.schema); pluginsService = new PluginsService({ coreId, env, logger, configService }); @@ -198,7 +198,7 @@ describe('PluginsService', () => { .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); mockDiscover.mockReturnValue({ error$: from([]), @@ -390,11 +390,10 @@ describe('PluginsService', () => { }); describe('#generateUiPluginsConfigs()', () => { - const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPluginInternal] => [ + const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPlugin] => [ plugin.name, { id: plugin.name, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: [], optionalPlugins: [], @@ -427,15 +426,14 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([plugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue({ - public: new Map([pluginToDiscoveredEntry(plugin)]), - internal: new Map([pluginToDiscoveredEntry(plugin)]), - }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); await pluginsService.discover(); - const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); - const uiConfig$ = uiPluginConfigs.get('plugin-with-expose'); + const uiConfig$ = browserConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); @@ -468,15 +466,55 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([plugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue({ - public: new Map([pluginToDiscoveredEntry(plugin)]), - internal: new Map([pluginToDiscoveredEntry(plugin)]), - }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); await pluginsService.discover(); - const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); - expect([...uiPluginConfigs.entries()]).toHaveLength(0); + expect([...browserConfigs.entries()]).toHaveLength(0); + }); + }); + + describe('#setup()', () => { + describe('uiPlugins.internal', () => { + it('includes disabled plugins', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-1', { + path: 'path-1', + version: 'some-version', + configPath: 'plugin1', + }), + createPlugin('plugin-2', { + path: 'path-2', + version: 'some-version', + configPath: 'plugin2', + }), + ]), + }); + + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + + config$.next( + new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } }) + ); + + await pluginsService.discover(); + const { uiPlugins } = await pluginsService.setup({} as any); + expect(uiPlugins.internal).toMatchInlineSnapshot(` + Map { + "plugin-1" => Object { + "entryPointPath": "path-1/public", + }, + "plugin-2" => Object { + "entryPointPath": "path-2/public", + }, + } + `); + }); }); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 79c9489a8b4c0..4c73c2a304dc4 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -25,12 +25,7 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; -import { - DiscoveredPlugin, - DiscoveredPluginInternal, - PluginConfigDescriptor, - PluginName, -} from './types'; +import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup } from '../internal_types'; @@ -41,10 +36,22 @@ import { pick } from '../../utils'; export interface PluginsServiceSetup { contracts: Map; uiPlugins: { + /** + * Paths to all discovered ui plugin entrypoints on the filesystem, even if + * disabled. + */ + internal: Map; + + /** + * Information needed by client-side to load plugins and wire dependencies. + */ public: Map; - internal: Map; + + /** + * Configuration for plugins to be exposed to the client-side. + */ + browserConfigs: Map>; }; - uiPluginConfigs: Map>; } /** @public */ @@ -65,6 +72,7 @@ export class PluginsService implements CoreService; private readonly pluginConfigDescriptors = new Map(); + private readonly uiPluginInternalInfo = new Map(); constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); @@ -103,8 +111,11 @@ export class PluginsService implements CoreService { expect(thirdPluginToRun.setup).toHaveBeenCalledTimes(1); }); -test('`uiPlugins` returns empty Maps before plugins are added', async () => { - expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(` - Object { - "internal": Map {}, - "public": Map {}, - } - `); +test('`uiPlugins` returns empty Map before plugins are added', async () => { + expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(`Map {}`); }); test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { @@ -351,7 +346,7 @@ test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { pluginsSystem.addPlugin(plugin); }); - expect([...pluginsSystem.uiPlugins().internal.keys()]).toMatchInlineSnapshot(` + expect([...pluginsSystem.uiPlugins().keys()]).toMatchInlineSnapshot(` Array [ "order-0", "order-1", @@ -380,7 +375,7 @@ test('`uiPlugins` returns only ui plugin dependencies', async () => { pluginsSystem.addPlugin(plugin); }); - const plugin = pluginsSystem.uiPlugins().internal.get('ui-plugin')!; + const plugin = pluginsSystem.uiPlugins().get('ui-plugin')!; expect(plugin.requiredPlugins).toEqual(['req-ui']); expect(plugin.optionalPlugins).toEqual(['opt-ui']); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 34acb66d4e931..f437b51e5b07a 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -17,12 +17,10 @@ * under the License. */ -import { pick } from 'lodash'; - import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -158,33 +156,22 @@ export class PluginsSystem { const uiPluginNames = [...this.getTopologicallySortedPluginNames().keys()].filter( pluginName => this.plugins.get(pluginName)!.includesUiPlugin ); - const internal = new Map( + const publicPlugins = new Map( uiPluginNames.map(pluginName => { const plugin = this.plugins.get(pluginName)!; return [ pluginName, { id: pluginName, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: plugin.manifest.requiredPlugins.filter(p => uiPluginNames.includes(p)), optionalPlugins: plugin.manifest.optionalPlugins.filter(p => uiPluginNames.includes(p)), }, - ] as [PluginName, DiscoveredPluginInternal]; + ]; }) ); - const publicPlugins = new Map( - [...internal.entries()].map( - ([pluginName, plugin]) => - [ - pluginName, - pick(plugin, ['id', 'configPath', 'requiredPlugins', 'optionalPlugins']), - ] as [PluginName, DiscoveredPlugin] - ) - ); - - return { public: publicPlugins, internal }; + return publicPlugins; } /** diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 17704ce687b92..fd487d9fe00aa 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -169,15 +169,14 @@ export interface DiscoveredPlugin { } /** - * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never - * be exposed to client-side code. * @internal */ -export interface DiscoveredPluginInternal extends DiscoveredPlugin { +export interface InternalPluginInfo { /** - * Path on the filesystem where plugin was loaded from. + * Path to the client-side entrypoint file to be used to build the client-side + * bundle for a plugin. */ - readonly path: string; + readonly entryPointPath: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7ecb9053a4bcf..066f79bfd38f3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1014,11 +1014,10 @@ export interface PluginsServiceSetup { // (undocumented) contracts: Map; // (undocumented) - uiPluginConfigs: Map>; - // (undocumented) uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; } @@ -1628,6 +1627,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:45:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts ``` diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 7399f2d08508f..9cc4e30d4252d 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -107,7 +107,6 @@ export default class KbnServer { __internals: { hapiServer: LegacyServiceSetupDeps['core']['http']['server']; uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; - uiPluginConfigs: LegacyServiceSetupDeps['core']['plugins']['uiPluginConfigs']; elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index c0885cd5d3d13..763167c6b5ccf 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -234,7 +234,7 @@ export function uiRenderMixin(kbnServer, server, config) { // Get the list of new platform plugins. // Convert the Map into an array of objects so it is JSON serializable and order is preserved. - const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPluginConfigs; + const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.browserConfigs; const uiPlugins = await Promise.all([ ...kbnServer.newPlatform.__internals.uiPlugins.public.entries(), ].map(async ([id, plugin]) => { diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 28eb448d12d82..2eaf4c1d6e882 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -61,7 +61,7 @@ export default class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; - this.discoveredPlugins = opts.discoveredPlugins; + this.newPlatformPluginInfo = opts.newPlatformPluginInfo; this.profile = opts.profile || false; this.workers = opts.workers; @@ -551,9 +551,9 @@ export default class BaseOptimizer { _getDiscoveredPluginEntryPoints() { // New platform plugin entry points - return [...this.discoveredPlugins.entries()] - .reduce((entryPoints, [pluginId, plugin]) => { - entryPoints[`plugin/${pluginId}`] = `${plugin.path}/public`; + return [...this.newPlatformPluginInfo.entries()] + .reduce((entryPoints, [pluginId, pluginInfo]) => { + entryPoints[`plugin/${pluginId}`] = pluginInfo.entryPointPath; return entryPoints; }, {}); } diff --git a/src/optimize/index.js b/src/optimize/index.js index 9789e7abc2f9d..0960f9ecb10b6 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -66,7 +66,7 @@ export default async (kbnServer, server, config) => { const optimizer = new FsOptimizer({ logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), uiBundles, - discoveredPlugins: newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index 16be840b3ca0e..9fbeceb578615 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -30,7 +30,7 @@ export default async (kbnServer, kibanaHapiServer, config) => { const watchOptimizer = new WatchOptimizer({ logWithMetadata, uiBundles: kbnServer.uiBundles, - discoveredPlugins: kbnServer.newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: kbnServer.newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), From aeb082e7c2ff2b2f24ee844a760894ef5c339085 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 25 Nov 2019 17:31:01 -0500 Subject: [PATCH 19/52] Resolving the a11y heading issue with edit/create user (#51538) --- .../views/management/edit_user/components/edit_user_page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx index 282ce4eea160c..91f5f048adc6d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx @@ -374,7 +374,7 @@ class EditUserPageUI extends Component { -

+

{isNewUser ? ( { values={{ userName: user.username }} /> )} -

+
{reserved && ( From 1da0dc8aec2f7e58031704daf219e24617ceac3f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 25 Nov 2019 16:31:42 -0600 Subject: [PATCH 20/52] Use real appBasePath in legacy AppService shim (#51353) --- src/legacy/ui/public/new_platform/new_platform.test.ts | 6 ++++-- src/legacy/ui/public/new_platform/new_platform.ts | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index cbdaccd65f94b..e5d5cd0a87776 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -18,13 +18,15 @@ */ import { setRootControllerMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__ } from './new_platform'; +import { legacyAppRegister, __reset__, __setup__ } from './new_platform'; +import { coreMock } from '../../../../core/public/mocks'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { beforeEach(() => { setRootControllerMock.mockReset(); __reset__(); + __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); }); const registerApp = () => { @@ -59,7 +61,7 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], - appBasePath: '', + appBasePath: '/test/base/path/app/test', }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index acf1191852dc8..36bfbcc7d5d46 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -111,7 +111,10 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + const unmount = await app.mount( + { core: npStart.core }, + { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) } + ); $scope.$on('$destroy', () => { unmount(); }); From fb476b26ebc9bceaa7e3d8f10a58505b7121b309 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 25 Nov 2019 15:44:49 -0700 Subject: [PATCH 21/52] skip flaky suite (#51669) --- .../apps/machine_learning/anomaly_detection/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index ba307a24cd739..2b76bce544f6d 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -6,7 +6,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - describe('anomaly detection', function() { + // FLAKY: https://github.com/elastic/kibana/issues/51669 + describe.skip('anomaly detection', function() { loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); From 71ed8567fb1133d2ea83f3899551ce3a6baffd48 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 25 Nov 2019 19:10:33 -0500 Subject: [PATCH 22/52] [SIEM] Signal _id generation to prevent duplicates / signal drops and addition of new constants (#51200) * updates how signal document id is generated through the result of sha256 hash of rule id plus source event id to prevent duplicate ids. Also adds constant for search_after result size to default to 100, and adds default maxSignals constant currently set to 100 signals. * updates bulk index to use bulk create for better duplicate handling, updates how each id is generated for each document inserted into signals index, updates error handling and logging statements * fix type check errors * allows docs with the same id / index but different versions to be created in signals index * adds tests for new id generation mechanism in signals index, optimizes search_after result sizing if total results in a signle results page is less than maxSignals, updates relevant tests * adds some comments * fixes failing tests after removing size from rest response * updates tests to push down generated uuids into test data, updates tests to ensure signal ids match static hash to ensure underlying changes to how the hash is generated are tested as well. * adds tests to ensure generated id is no larger than 512 bytes * replaces splice with slice in unit tests * updates id generator to include rule_id --- .../legacy/plugins/siem/common/constants.ts | 2 + .../alerts/__mocks__/es_results.ts | 130 ++++++-- .../detection_engine/alerts/create_signals.ts | 1 - .../alerts/signals_alert_type.ts | 21 +- .../lib/detection_engine/alerts/types.ts | 42 ++- .../lib/detection_engine/alerts/utils.test.ts | 287 +++++++++++++----- .../lib/detection_engine/alerts/utils.ts | 96 ++++-- .../routes/create_signals_route.ts | 2 - .../lib/detection_engine/routes/schemas.ts | 3 +- .../routes/update_signals_route.ts | 2 - .../lib/detection_engine/routes/utils.test.ts | 9 - .../lib/detection_engine/routes/utils.ts | 1 - 12 files changed, 443 insertions(+), 153 deletions(-) diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index f01493cec869e..e5d1fc83dac26 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -17,6 +17,8 @@ export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; export const DEFAULT_SIGNALS_INDEX_KEY = 'siem:defaultSignalsIndex'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +export const DEFAULT_MAX_SIGNALS = 100; +export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 0a70a7342b2dd..7d3b51c071c09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -30,33 +30,43 @@ export const sampleSignalAlertParams = ( filters: undefined, savedId: undefined, meta: undefined, - size: 1000, }); -export const sampleDocNoSortId: SignalSourceHit = { +export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, -}; +}); + +export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _id: someUuid, + _source: { + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + }, +}); -export const sampleDocWithSortId: SignalSourceHit = { +export const sampleDocWithSortId = (someUuid: string): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, sort: ['1234567891111'], -}; +}); export const sampleEmptyDocSearchResults: SignalSearchResponse = { took: 10, @@ -74,7 +84,61 @@ export const sampleEmptyDocSearchResults: SignalSearchResponse = { }, }; -export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { +export const sampleBulkCreateDuplicateResult = { + took: 60, + errors: true, + items: [ + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + _version: 1, + result: 'created', + _shards: { + total: 2, + successful: 1, + failed: 0, + }, + _seq_no: 1, + _primary_term: 1, + status: 201, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + ], +}; + +export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -88,13 +152,35 @@ export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); + +export const sampleDocSearchResultsNoSortIdNoVersion = ( + someUuid: string +): SignalSearchResponse => ({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 100, + max_score: 100, + hits: [ + { + ...sampleDocNoSortIdNoVersion(someUuid), + }, + ], + }, +}); -export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { +export const sampleDocSearchResultsNoSortIdNoHits = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -108,13 +194,17 @@ export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); -export const repeatedSearchResultsWithSortId = (repeat: number) => ({ +export const repeatedSearchResultsWithSortId = ( + total: number, + pageSize: number, + guids: string[] +) => ({ took: 10, timed_out: false, _shards: { @@ -124,15 +214,15 @@ export const repeatedSearchResultsWithSortId = (repeat: number) => ({ skipped: 0, }, hits: { - total: repeat, + total, max_score: 100, - hits: Array.from({ length: repeat }).map(x => ({ - ...sampleDocWithSortId, + hits: Array.from({ length: pageSize }).map((x, index) => ({ + ...sampleDocWithSortId(guids[index]), })), }, }); -export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { +export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -146,10 +236,10 @@ export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocWithSortId, + ...sampleDocWithSortId(someUuid), }, ], }, -}; +}); export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 420f995431423..8770282356cf5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -29,7 +29,6 @@ export const createSignals = async ({ outputIndex, name, severity, - size, tags, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index dfc779329d3b2..69eb3eb665060 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -6,10 +6,14 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; -import { SIGNALS_ID } from '../../../../common/constants'; +import { + SIGNALS_ID, + DEFAULT_MAX_SIGNALS, + DEFAULT_SEARCH_AFTER_PAGE_SIZE, +} from '../../../../common/constants'; import { buildEventsSearchQuery } from './build_events_query'; -import { searchAfterAndBulkIndex } from './utils'; +import { searchAfterAndBulkCreate } from './utils'; import { SignalAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; import { getInputOutputIndex } from './get_input_output_index'; @@ -40,14 +44,13 @@ export const signalsAlertType = ({ meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - maxSignals: schema.number({ defaultValue: 10000 }), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), riskScore: schema.number(), severity: schema.string(), tags: schema.arrayOf(schema.string(), { defaultValue: [] }), to: schema.string(), type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), - size: schema.maybe(schema.number()), }), }, async executor({ alertId, services, params }) { @@ -63,7 +66,6 @@ export const signalsAlertType = ({ query, to, type, - size, } = params; // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 @@ -75,7 +77,11 @@ export const signalsAlertType = ({ const interval: string = savedObject.attributes.interval; const enabled: boolean = savedObject.attributes.enabled; - const searchAfterSize = size ? size : 1000; + // set searchAfter page size to be the lesser of default page size or maxSignals. + const searchAfterSize = + DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals + ? DEFAULT_SEARCH_AFTER_PAGE_SIZE + : params.maxSignals; const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( services, @@ -119,7 +125,7 @@ export const signalsAlertType = ({ ); } - const bulkIndexResult = await searchAfterAndBulkIndex({ + const bulkIndexResult = await searchAfterAndBulkCreate({ someResult: noReIndexResult, signalParams: params, services, @@ -131,6 +137,7 @@ export const signalsAlertType = ({ updatedBy, interval, enabled, + pageSize: searchAfterSize, }); if (bulkIndexResult) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 9c6e1f99c672b..29eb7872f163d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -42,7 +42,6 @@ export interface SignalAlertParams { savedId: string | undefined | null; meta: Record | undefined | null; severity: string; - size: number | undefined | null; tags: string[]; to: string; type: 'filter' | 'query' | 'saved_query'; @@ -173,7 +172,46 @@ export interface SignalSource { export interface BulkResponse { took: number; errors: boolean; - items: unknown[]; + items: [ + { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; + } + ]; +} + +export interface MGetResponse { + docs: GetResponse[]; +} +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _seq_no: number; + _primary_term: number; + found: boolean; + _source: SearchTypes; } export type SignalSearchResponse = SearchResponse; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index bc147fa1dae07..4aac425c7f80f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -3,23 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import uuid from 'uuid'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { Logger } from '../../../../../../../../src/core/server'; import { buildBulkBody, - singleBulkIndex, + generateId, + singleBulkCreate, singleSearchAfter, - searchAfterAndBulkIndex, + searchAfterAndBulkCreate, } from './utils'; import { sampleDocNoSortId, sampleSignalAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, + sampleDocSearchResultsNoSortIdNoVersion, sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, + sampleBulkCreateDuplicateResult, sampleSignalId, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; @@ -46,9 +51,10 @@ describe('utils', () => { }); describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { + const fakeUuid = uuid.v4(); const sampleParams = sampleSignalAlertParams(undefined); const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId, + doc: sampleDocNoSortId(fakeUuid), signalParams: sampleParams, id: sampleSignalId, name: 'rule-name', @@ -59,11 +65,13 @@ describe('utils', () => { }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; + if (fakeSignalSourceHit.signal.parent) { + delete fakeSignalSourceHit.signal.parent.id; + } expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', signal: { parent: { - id: 'someFakeId', type: 'event', index: 'myFakeSignalIndex', depth: 1, @@ -88,7 +96,6 @@ describe('utils', () => { severity: 'high', tags: ['some fake tag'], type: 'query', - size: 1000, status: 'open', to: 'now', enabled: true, @@ -99,8 +106,114 @@ describe('utils', () => { }); }); }); - describe('singleBulkIndex', () => { - test('create successful bulk index', async () => { + describe('singleBulkCreate', () => { + describe('create signal id gereateId', () => { + test('two docs with same index, id, and version should have same id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId); + expect(firstHash).toEqual(generatedHash); + expect(secondHash).toEqual(generatedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + }); + test('two docs with different index, id, and version should have different id', () => { + const findex = 'myfakeindex'; + const findex2 = 'mysecondfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'mysecondfakeindexsomefakeid1rule-1' + const secondGeneratedHash = + 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex2, fid, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, different id, and same version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const fid2 = 'somefakeid2'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'myfakeindexsomefakeid21rule-1' + const secondGeneratedHash = + '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid2, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, same id, and different version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const version2 = '2'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid2rule-1' + const secondGeneratedHash = + 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version2, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { + const longIndexName = + 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const firstHash = generateId(longIndexName, fid, version, ruleId); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + }); + test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const ruleId2 = 'rule-2'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid1rule-2' + const secondGeneratedHash = + '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId2); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + }); + test('create successful bulk create', async () => { + const fakeUuid = uuid.v4(); const sampleParams = sampleSignalAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ @@ -112,8 +225,36 @@ describe('utils', () => { }, ], }); - const successfulSingleBulkIndex = await singleBulkIndex({ - someResult: sampleSearchResult, + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), + signalParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleSignalId, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create successful bulk create with docs with no versioning', async () => { + const fakeUuid = uuid.v4(); + const sampleParams = sampleSignalAlertParams(undefined); + const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -125,13 +266,13 @@ describe('utils', () => { interval: '5m', enabled: true, }); - expect(successfulSingleBulkIndex).toEqual(true); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to empty search results', async () => { + test('create unsuccessful bulk create due to empty search results', async () => { const sampleParams = sampleSignalAlertParams(undefined); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); - const successfulSingleBulkIndex = await singleBulkIndex({ + const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult, signalParams: sampleParams, services: mockService, @@ -144,18 +285,15 @@ describe('utils', () => { interval: '5m', enabled: true, }); - expect(successfulSingleBulkIndex).toEqual(true); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to bulk index errors', async () => { - // need a sample search result, sample signal params, mock service, mock logger + test('create successful bulk create when bulk create has errors', async () => { + const fakeUuid = uuid.v4(); const sampleParams = sampleSignalAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, - }); - const successfulSingleBulkIndex = await singleBulkIndex({ - someResult: sampleSearchResult, + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -168,7 +306,7 @@ describe('utils', () => { enabled: true, }); expect(mockLogger.error).toHaveBeenCalled(); - expect(successfulSingleBulkIndex).toEqual(false); + expect(successfulsingleBulkCreate).toEqual(true); }); }); describe('singleSearchAfter', () => { @@ -182,6 +320,7 @@ describe('utils', () => { signalParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }) ).rejects.toThrow('Attempted to search after with empty sort id'); }); @@ -194,6 +333,7 @@ describe('utils', () => { signalParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }); expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); }); @@ -209,14 +349,15 @@ describe('utils', () => { signalParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }) ).rejects.toThrow('Fake Error'); }); }); - describe('searchAfterAndBulkIndex', () => { + describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { const sampleParams = sampleSignalAlertParams(undefined); - const result = await searchAfterAndBulkIndex({ + const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, signalParams: sampleParams, services: mockService, @@ -228,12 +369,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleSignalAlertParams(30); + const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -244,7 +387,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) .mockReturnValueOnce({ took: 100, errors: false, @@ -254,7 +397,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) .mockReturnValueOnce({ took: 100, errors: false, @@ -264,8 +407,8 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -276,18 +419,17 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(result).toEqual(true); }); - test('if unsuccessful first bulk index', async () => { + test('if unsuccessful first bulk create', async () => { + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); const sampleParams = sampleSignalAlertParams(10); - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, // will cause singleBulkIndex to return false - }); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -298,13 +440,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids', async () => { + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { const sampleParams = sampleSignalAlertParams(undefined); - + const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -314,8 +457,8 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: sampleDocSearchResultsNoSortId, + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortId(someUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -326,12 +469,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids and 0 total hits', async () => { + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { const sampleParams = sampleSignalAlertParams(undefined); + const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -341,8 +486,8 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: sampleDocSearchResultsNoSortIdNoHits, + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -353,11 +498,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { const sampleParams = sampleSignalAlertParams(10); + const oneGuid = uuid.v4(); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -368,9 +516,9 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -381,11 +529,13 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { const sampleParams = sampleSignalAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -397,8 +547,8 @@ describe('utils', () => { ], }) .mockReturnValueOnce(sampleEmptyDocSearchResults); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -409,41 +559,13 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); - test('if logs error when iteration is unsuccessful when bulk index results in a failure', async () => { - const sampleParams = sampleSignalAlertParams(5); - mockService.callCluster - .mockReturnValueOnce({ - // first bulk insert - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(sampleDocSearchResultsWithSortId); // get some more docs - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleSignalId, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(result).toEqual(true); - }); test('if returns false when singleSearchAfter throws an exception', async () => { const sampleParams = sampleSignalAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -454,9 +576,11 @@ describe('utils', () => { }, ], }) - .mockRejectedValueOnce(Error('Fake Error')); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + .mockImplementation(() => { + throw Error('Fake Error'); + }); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -467,6 +591,7 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(false); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 25934dc9aa356..f2a3424655945 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { createHash } from 'crypto'; import { performance } from 'perf_hooks'; import { pickBy } from 'lodash/fp'; import { SignalHit, Signal } from '../../types'; @@ -59,7 +60,6 @@ export const buildRule = ({ severity: signalParams.severity, tags: signalParams.tags, type: signalParams.type, - size: signalParams.size, to: signalParams.to, enabled, filters: signalParams.filters, @@ -121,7 +121,7 @@ export const buildBulkBody = ({ return signalHit; }; -interface SingleBulkIndexParams { +interface SingleBulkCreateParams { someResult: SignalSearchResponse; signalParams: AlertTypeParams; services: AlertServices; @@ -135,8 +135,18 @@ interface SingleBulkIndexParams { enabled: boolean; } +export const generateId = ( + docIndex: string, + docId: string, + version: string, + ruleId: string +): string => + createHash('sha256') + .update(docIndex.concat(docId, version, ruleId)) + .digest('hex'); + // Bulk Index documents. -export const singleBulkIndex = async ({ +export const singleBulkCreate = async ({ someResult, signalParams, services, @@ -148,15 +158,29 @@ export const singleBulkIndex = async ({ updatedBy, interval, enabled, -}: SingleBulkIndexParams): Promise => { +}: SingleBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } + // index documents after creating an ID based on the + // source documents' originating index, and the original + // document _id. This will allow two documents from two + // different indexes with the same ID to be + // indexed, and prevents us from creating any updates + // to the documents once inserted into the signals index, + // while preventing duplicates from being added to the + // signals index if rules are re-run over the same time + // span. Also allow for versioning. const bulkBody = someResult.hits.hits.flatMap(doc => [ { - index: { + create: { _index: signalsIndex, - _id: doc._id, + _id: generateId( + doc._index, + doc._id, + doc._version ? doc._version.toString() : '', + signalParams.ruleId ?? '' + ), }, }, buildBulkBody({ doc, signalParams, id, name, createdBy, updatedBy, interval, enabled }), @@ -171,8 +195,27 @@ export const singleBulkIndex = async ({ logger.debug(`individual bulk process time took: ${time2 - time1} milliseconds`); logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); if (firstResult.errors) { - logger.error(`[-] bulkResponse had errors: ${JSON.stringify(firstResult.errors, null, 2)}`); - return false; + // go through the response status errors and see what + // types of errors they are, count them up, and log them. + const errorCountMap = firstResult.items.reduce((acc: { [key: string]: number }, item) => { + if (item.create.error) { + const responseStatusKey = item.create.status.toString(); + acc[responseStatusKey] = acc[responseStatusKey] ? acc[responseStatusKey] + 1 : 1; + } + return acc; + }, {}); + /* + the logging output below should look like + {'409': 55} + which is read as "there were 55 counts of 409 errors returned from bulk create" + */ + logger.error( + `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( + errorCountMap, + null, + 2 + )}` + ); } return true; }; @@ -182,6 +225,7 @@ interface SingleSearchAfterParams { signalParams: AlertTypeParams; services: AlertServices; logger: Logger; + pageSize: number; } // utilize search_after for paging results into bulk. @@ -190,6 +234,7 @@ export const singleSearchAfter = async ({ signalParams, services, logger, + pageSize, }: SingleSearchAfterParams): Promise => { if (searchAfterSortId == null) { throw Error('Attempted to search after with empty sort id'); @@ -200,7 +245,7 @@ export const singleSearchAfter = async ({ from: signalParams.from, to: signalParams.to, filter: signalParams.filter, - size: signalParams.size ? signalParams.size : 1000, + size: pageSize, searchAfterSortId, }); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( @@ -214,7 +259,7 @@ export const singleSearchAfter = async ({ } }; -interface SearchAfterAndBulkIndexParams { +interface SearchAfterAndBulkCreateParams { someResult: SignalSearchResponse; signalParams: AlertTypeParams; services: AlertServices; @@ -226,10 +271,11 @@ interface SearchAfterAndBulkIndexParams { updatedBy: string; interval: string; enabled: boolean; + pageSize: number; } // search_after through documents and re-index using bulk endpoint. -export const searchAfterAndBulkIndex = async ({ +export const searchAfterAndBulkCreate = async ({ someResult, signalParams, services, @@ -241,13 +287,14 @@ export const searchAfterAndBulkIndex = async ({ updatedBy, interval, enabled, -}: SearchAfterAndBulkIndexParams): Promise => { + pageSize, +}: SearchAfterAndBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } logger.debug('[+] starting bulk insertion'); - const firstBulkIndexSuccess = await singleBulkIndex({ + await singleBulkCreate({ someResult, signalParams, services, @@ -260,18 +307,15 @@ export const searchAfterAndBulkIndex = async ({ interval, enabled, }); - if (!firstBulkIndexSuccess) { - logger.error('First bulk index was unsuccessful'); - return false; - } - const totalHits = typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; // maxTotalHitsSize represents the total number of docs to - // query for. If maxSignals is present we will only query - // up to max signals - otherwise use the value - // from track_total_hits. - const maxTotalHitsSize = signalParams.maxSignals ? signalParams.maxSignals : totalHits; + // query for, no matter the size of each individual page of search results. + // If the total number of hits for the overall search result is greater than + // maxSignals, default to requesting a total of maxSignals, otherwise use the + // totalHits in the response from the searchAfter query. + const maxTotalHitsSize = + totalHits >= signalParams.maxSignals ? signalParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -295,6 +339,7 @@ export const searchAfterAndBulkIndex = async ({ signalParams, services, logger, + pageSize, // maximum number of docs to receive per search result. }); if (searchAfterResult.hits.hits.length === 0) { return true; @@ -308,7 +353,7 @@ export const searchAfterAndBulkIndex = async ({ } sortId = sortIds[0]; logger.debug('next bulk index'); - const bulkSuccess = await singleBulkIndex({ + await singleBulkCreate({ someResult: searchAfterResult, signalParams, services, @@ -322,14 +367,11 @@ export const searchAfterAndBulkIndex = async ({ enabled, }); logger.debug('finished next bulk index'); - if (!bulkSuccess) { - logger.error('[-] bulk index failed but continuing'); - } } catch (exc) { logger.error(`[-] search_after and bulk threw an error ${exc}`); return false; } } - logger.debug(`[+] completed bulk index of ${totalHits}`); + logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); return true; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts index 7b6559561c783..fa8fd66ef2aef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts @@ -49,7 +49,6 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { risk_score: riskScore, name, severity, - size, tags, to, type, @@ -92,7 +91,6 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { riskScore, name, severity, - size, tags, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 210ce5ca9fdce..177e7cbebc213 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; /* eslint-disable @typescript-eslint/camelcase */ const description = Joi.string(); @@ -103,7 +104,7 @@ export const createSignalsSchema = Joi.object({ }), meta, risk_score: risk_score.required(), - max_signals: max_signals.default(100), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), name: name.required(), severity: severity.required(), tags: tags.default([]), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts index 274c41f65a36b..1dc54f34bd1f7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts @@ -47,7 +47,6 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { risk_score: riskScore, name, severity, - size, tags, to, type, @@ -84,7 +83,6 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { riskScore, name, severity, - size, tags, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 22dd7be5fbba7..ed9e00735c704 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -37,7 +37,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -65,7 +64,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -95,7 +93,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -125,7 +122,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -153,7 +149,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -184,7 +179,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -215,7 +209,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -297,7 +290,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -335,7 +327,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index e3a677741efca..7b9921b0375d8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -50,7 +50,6 @@ export const transformAlertToSignal = (signal: SignalAlertType): Partial Date: Mon, 25 Nov 2019 16:12:30 -0800 Subject: [PATCH 23/52] Chore/remove once per server (#49426) * [Reporting/np-k8] Remove several oncePerServer usages * ts fixes 1 * ts fixes 2 * more ts fixes * more ts fixes * more ts fixes * ts simplification * improve ts * remove any type for jobParams and define JobParamsSavedObject and JobParamsUrl * ts simplification * Fix ts * ts simplification * fix ts * bug fix * align with joels pr * Move get_absolute_url to not use oncePerServer * Two more removals of oncePerServer * Final once-per-server removals * AbsoluteURLFactory => AbsoluteURLFactoryOptions * Fix absolute_url util --- .../reporting/common/get_absolute_url.test.ts | 60 ++++++++----------- .../reporting/common/get_absolute_url.ts | 21 ++++--- .../common/execute_job/get_full_urls.ts | 9 ++- .../routes/lib/authorized_user_pre_routing.js | 7 +-- .../lib/reporting_feature_pre_routing.js | 7 +-- x-pack/legacy/plugins/reporting/types.d.ts | 7 +++ 6 files changed, 57 insertions(+), 54 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts index 9bad3b2b08002..cb792fbd6ae03 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts @@ -4,88 +4,80 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../test_helpers/create_mock_server'; import { getAbsoluteUrlFactory } from './get_absolute_url'; -test(`by default it builds url using information from server.info.protocol and the server.config`, () => { - const mockServer = createMockServer(''); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); +const defaultOptions = { + defaultBasePath: 'sbp', + protocol: 'http:', + hostname: 'localhost', + port: 5601, +}; + +test(`by default it builds urls using information from server.info.protocol and the server.config`, () => { + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.protocol if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.protocol': 'https', - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + protocol: 'https:', + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`https://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.hostname if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.hostname': 'something-else', - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + hostname: 'something-else', + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://something-else:5601/sbp/app/kibana`); }); test(`uses kibanaServer.port if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.port': 8008, - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + port: 8008, + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:8008/sbp/app/kibana`); }); test(`uses the provided hash`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const hash = '/hash'; const absoluteUrl = getAbsoluteUrl({ hash }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided hash with queryString`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const hash = '/hash?querystring'; const absoluteUrl = getAbsoluteUrl({ hash }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided basePath`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' }); expect(absoluteUrl).toBe(`http://localhost:5601/s/marketing/app/kibana`); }); test(`uses the path`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const path = '/app/canvas'; const absoluteUrl = getAbsoluteUrl({ path }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp${path}`); }); test(`uses the search`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const search = '_t=123456789'; const absoluteUrl = getAbsoluteUrl({ search }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana?${search}`); diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts index 1d34189abcb24..0e350cb1ec011 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts @@ -5,24 +5,27 @@ */ import url from 'url'; -import { ServerFacade } from '../types'; - -export function getAbsoluteUrlFactory(server: ServerFacade) { - const config = server.config(); +import { AbsoluteURLFactoryOptions } from '../types'; +export const getAbsoluteUrlFactory = ({ + protocol, + hostname, + port, + defaultBasePath, +}: AbsoluteURLFactoryOptions) => { return function getAbsoluteUrl({ - basePath = config.get('server.basePath'), + basePath = defaultBasePath, hash = '', path = '/app/kibana', search = '', } = {}) { return url.format({ - protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, - hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + protocol, + hostname, + port, pathname: basePath + path, hash, search, }); }; -} +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts index 3b82852073421..2b66c77067ed2 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts @@ -28,7 +28,14 @@ export async function getFullUrls({ job: JobDocPayloadPNG | JobDocPayloadPDF; server: ServerFacade; }) { - const getAbsoluteUrl = getAbsoluteUrlFactory(server); + const config = server.config(); + + const getAbsoluteUrl = getAbsoluteUrlFactory({ + defaultBasePath: config.get('server.basePath'), + protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, + hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), + port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + }); // PDF and PNG job params put in the url differently let relativeUrls: string[] = []; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js index 10ff9f477f424..59317ac46773b 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js @@ -6,11 +6,10 @@ import boom from 'boom'; import { getUserFactory } from '../../lib/get_user'; -import { oncePerServer } from '../../lib/once_per_server'; const superuserRole = 'superuser'; -function authorizedUserPreRoutingFn(server) { +export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn(server) { const getUser = getUserFactory(server); const config = server.config(); @@ -40,6 +39,4 @@ function authorizedUserPreRoutingFn(server) { return user; }; -} - -export const authorizedUserPreRoutingFactory = oncePerServer(authorizedUserPreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js index ad91e5a654a4e..92973e3d0b422 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js @@ -5,9 +5,8 @@ */ import Boom from 'boom'; -import { oncePerServer } from '../../lib/once_per_server'; -function reportingFeaturePreRoutingFn(server) { +export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn(server) { const xpackMainPlugin = server.plugins.xpack_main; const pluginId = 'reporting'; @@ -24,6 +23,4 @@ function reportingFeaturePreRoutingFn(server) { } }; }; -} - -export const reportingFeaturePreRoutingFactory = oncePerServer(reportingFeaturePreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 7d05811ef4aa6..e8fb015426f51 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -326,3 +326,10 @@ export { CancellationToken } from './common/cancellation_token'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger } from './server/lib/level_logger'; + +export interface AbsoluteURLFactoryOptions { + defaultBasePath: string; + protocol: string; + hostname: string; + port: string | number; +} From ab3944c38a2603dce71f6f653197bea9b0320f4b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Nov 2019 18:03:48 -0700 Subject: [PATCH 24/52] [Maps] a11y header fixes (#51511) --- x-pack/legacy/plugins/maps/public/angular/map.html | 2 ++ x-pack/legacy/plugins/maps/public/angular/map_controller.js | 1 + 2 files changed, 3 insertions(+) diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/legacy/plugins/maps/public/angular/map.html index 90d4ddbeb0092..2f34ffa660d6e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map.html +++ b/x-pack/legacy/plugins/maps/public/angular/map.html @@ -1,4 +1,5 @@
+
+

{{screenTitle}}

diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 41c618d68a68e..b9354dd0a0ddd 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -66,6 +66,7 @@ const app = uiModules.get(MAP_APP_PATH, []); app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { const { filterManager } = npStart.plugins.data.query; const savedMap = $route.current.locals.map; + $scope.screenTitle = savedMap.title; let unsubscribe; let initialLayerListConfig; const $state = new AppState(); From e3a97ddd872a8d8aed5e2a2e834c57b2cfc9f459 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 25 Nov 2019 18:17:24 -0700 Subject: [PATCH 25/52] Typescriptify search source (#47644) * Initial refactor of search source * Add abort signal to search source fetch and remove cancel queued * Remove usages of Angular Promises * Remove usages of angular "sessionId" service * Remove config as dependency * Update deps on config and esShardTimeout * Remove remaining angular dependencies from SearchSource * Fix Karma tests * Separate callClient and handleResponse and add tests for each * Add tests for fetchSoon * Add back search source test and convert to jest * Create search strategy registry test * Revert empty test * Remove filter predicates from search source * Update typings and throw response errors * Fix proxy to properly return response from ES * Update jest snapshots * Remove unused translations * Don't pass search request to onRequestStart, create it afterwards * Fix search source & get search params tests * Fix issue with angular scope not firing after setting state on vis * Fix issue with angular scope not firing after setting state on vis * Typescriptify courier/search source * Fix references * Fix types * Fix removal of underscores * Fix fetching * Fix tag cloud vis * Fix setting of visConfig to not happen async * Remove unused snapshots * Remove courier and search poller * Update types * Fix issue with filters not applying * Fix issue with search embeddable time ranges * Remove deleted file again * Fix source includes * Fix searchsource constructor * Don't pass null as initial value for search source * Fix inmemorytable type * Fix issue with filters aggregation * Fix tests * Mock new platform * Fix agg filters * Fix context app tests * Fix context tests (for real) * Fix normalizeSortRequest for dateNanos * Fix issue with multiple levels of Other buckets * Adding in last commit * Review feedback * Update references to ui/courier * Fix eslint * Fix tests * Fix filter matches index for filters with partial meta * Fix type errors * Fix references * Address review feedback * Fix failing test --- src/fixtures/stubbed_search_source.js | 3 - .../data/public/search/expressions/esaggs.ts | 15 +- .../public/control/create_search_source.js | 4 +- .../saved_dashboard/saved_dashboard.d.ts | 4 +- .../angular/context/api/__tests__/anchor.js | 2 +- .../context/api/__tests__/predecessors.js | 2 +- .../context/api/__tests__/successors.js | 2 +- .../discover/angular/context/api/anchor.js | 2 +- .../discover/angular/context/api/context.ts | 5 +- .../api/utils/__tests__/sorting.test.ts | 5 +- .../api/utils/fetch_hits_in_interval.ts | 12 +- .../context/api/utils/generate_intervals.ts | 2 +- .../context/api/utils/get_es_query_sort.ts | 7 +- .../angular/context/api/utils/sorting.ts | 6 +- .../discover/embeddable/search_embeddable.ts | 12 +- .../embeddable/search_embeddable_factory.ts | 3 +- .../kibana/public/discover/kibana_services.ts | 10 +- .../kibana/public/discover/types.d.ts | 5 +- .../embeddable/visualize_embeddable.ts | 4 +- src/legacy/ui/public/agg_types/agg_config.ts | 5 +- src/legacy/ui/public/agg_types/agg_configs.ts | 3 +- .../ui/public/agg_types/buckets/terms.ts | 6 +- .../ui/public/agg_types/param_types/base.ts | 6 +- ...all_client.test.js => call_client.test.ts} | 87 ++- .../fetch/{call_client.js => call_client.ts} | 30 +- .../fetch/components/shard_failure_types.ts | 1 + src/legacy/ui/public/courier/fetch/errors.ts | 9 +- .../ui/public/courier/fetch/fetch_soon.js | 70 --- ...{fetch_soon.test.js => fetch_soon.test.ts} | 74 +-- .../ui/public/courier/fetch/fetch_soon.ts | 83 +++ ...rams.test.js => get_search_params.test.ts} | 7 +- ..._search_params.js => get_search_params.ts} | 18 +- ...sponse.test.js => handle_response.test.ts} | 24 +- ...handle_response.js => handle_response.tsx} | 30 +- .../courier/fetch/{index.js => index.ts} | 0 src/legacy/ui/public/courier/fetch/types.ts | 41 ++ .../public/courier/{index.d.ts => index.ts} | 2 + .../public/courier/search_poll/search_poll.js | 68 --- .../__tests__/normalize_sort_request.js | 124 ---- .../search_source/_normalize_sort_request.js | 88 --- ...test.js => filter_docvalue_fields.test.ts} | 7 +- ...ue_fields.js => filter_docvalue_fields.ts} | 10 +- .../public/courier/search_source/index.d.ts | 20 - .../search_source/{index.js => index.ts} | 2 +- .../ui/public/courier/search_source/mocks.ts | 19 +- .../normalize_sort_request.test.ts | 142 +++++ .../search_source/normalize_sort_request.ts | 78 +++ .../courier/search_source/search_source.d.ts | 39 -- .../courier/search_source/search_source.js | 540 ------------------ .../search_source/search_source.test.js | 193 ------- .../search_source/search_source.test.ts | 156 +++++ .../courier/search_source/search_source.ts | 410 +++++++++++++ .../ui/public/courier/search_source/types.ts | 106 ++++ ...est.js => default_search_strategy.test.ts} | 55 +- ...strategy.js => default_search_strategy.ts} | 28 +- .../search_strategy/{index.js => index.ts} | 0 ...rn.js => is_default_type_index_pattern.ts} | 4 +- ...h_strategy.js => no_op_search_strategy.ts} | 17 +- .../courier/search_strategy/search_error.d.ts | 21 - .../{search_error.js => search_error.ts} | 21 +- ...st.js => search_strategy_registry.test.ts} | 75 ++- ...egistry.js => search_strategy_registry.ts} | 24 +- .../public/courier/search_strategy/types.ts | 37 ++ .../{search_strategy/index.d.ts => types.ts} | 5 +- .../utils/courier_inspector_utils.d.ts | 52 -- ...or_utils.js => courier_inspector_utils.ts} | 45 +- .../courier/{index.js => utils/types.ts} | 21 +- .../__tests__/field_wildcard.js | 126 ---- .../field_wildcard/field_wildcard.test.ts | 86 +++ .../{field_wildcard.js => field_wildcard.ts} | 18 +- .../field_wildcard/{index.js => index.ts} | 0 src/legacy/ui/public/promises/defer.ts | 2 +- .../components/visualization_requesterror.tsx | 4 +- .../pipeline_helpers/build_pipeline.test.ts | 2 +- .../loader/pipeline_helpers/build_pipeline.ts | 9 +- .../loader/utils/query_geohash_bounds.ts | 4 +- .../es_query/filter_matches_index.test.ts | 7 + .../es_query/es_query/filter_matches_index.ts | 2 +- .../common/es_query/filters/range_filter.ts | 24 +- .../utils/highlight/highlight_request.test.ts | 21 +- .../utils/highlight/highlight_request.ts | 4 +- .../public/query/timefilter/get_time.test.ts | 6 +- .../data/public/query/timefilter/get_time.ts | 39 +- x-pack/legacy/plugins/infra/types/eui.d.ts | 1 + .../plugins/maps/public/kibana_services.js | 4 +- .../contexts/kibana/__mocks__/saved_search.ts | 3 +- .../datavisualizer/index_based/page.tsx | 2 +- .../jobs/new_job/utils/new_job_utils.ts | 10 +- .../services/new_job_capabilities_service.ts | 2 +- .../plugins/rollup/public/search/register.js | 2 +- .../public/search/rollup_search_strategy.js | 2 +- 91 files changed, 1646 insertions(+), 1742 deletions(-) rename src/legacy/ui/public/courier/fetch/{call_client.test.js => call_client.test.ts} (64%) rename src/legacy/ui/public/courier/fetch/{call_client.js => call_client.ts} (70%) delete mode 100644 src/legacy/ui/public/courier/fetch/fetch_soon.js rename src/legacy/ui/public/courier/fetch/{fetch_soon.test.js => fetch_soon.test.ts} (63%) create mode 100644 src/legacy/ui/public/courier/fetch/fetch_soon.ts rename src/legacy/ui/public/courier/fetch/{get_search_params.test.js => get_search_params.test.ts} (96%) rename src/legacy/ui/public/courier/fetch/{get_search_params.js => get_search_params.ts} (73%) rename src/legacy/ui/public/courier/fetch/{handle_response.test.js => handle_response.test.ts} (78%) rename src/legacy/ui/public/courier/fetch/{handle_response.js => handle_response.tsx} (71%) rename src/legacy/ui/public/courier/fetch/{index.js => index.ts} (100%) create mode 100644 src/legacy/ui/public/courier/fetch/types.ts rename src/legacy/ui/public/courier/{index.d.ts => index.ts} (94%) delete mode 100644 src/legacy/ui/public/courier/search_poll/search_poll.js delete mode 100644 src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js delete mode 100644 src/legacy/ui/public/courier/search_source/_normalize_sort_request.js rename src/legacy/ui/public/courier/search_source/{filter_docvalue_fields.test.js => filter_docvalue_fields.test.ts} (88%) rename src/legacy/ui/public/courier/search_source/{filter_docvalue_fields.js => filter_docvalue_fields.ts} (84%) delete mode 100644 src/legacy/ui/public/courier/search_source/index.d.ts rename src/legacy/ui/public/courier/search_source/{index.js => index.ts} (94%) create mode 100644 src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts create mode 100644 src/legacy/ui/public/courier/search_source/normalize_sort_request.ts delete mode 100644 src/legacy/ui/public/courier/search_source/search_source.d.ts delete mode 100644 src/legacy/ui/public/courier/search_source/search_source.js delete mode 100644 src/legacy/ui/public/courier/search_source/search_source.test.js create mode 100644 src/legacy/ui/public/courier/search_source/search_source.test.ts create mode 100644 src/legacy/ui/public/courier/search_source/search_source.ts create mode 100644 src/legacy/ui/public/courier/search_source/types.ts rename src/legacy/ui/public/courier/search_strategy/{default_search_strategy.test.js => default_search_strategy.test.ts} (67%) rename src/legacy/ui/public/courier/search_strategy/{default_search_strategy.js => default_search_strategy.ts} (76%) rename src/legacy/ui/public/courier/search_strategy/{index.js => index.ts} (100%) rename src/legacy/ui/public/courier/search_strategy/{is_default_type_index_pattern.js => is_default_type_index_pattern.ts} (85%) rename src/legacy/ui/public/courier/search_strategy/{no_op_search_strategy.js => no_op_search_strategy.ts} (79%) delete mode 100644 src/legacy/ui/public/courier/search_strategy/search_error.d.ts rename src/legacy/ui/public/courier/search_strategy/{search_error.js => search_error.ts} (76%) rename src/legacy/ui/public/courier/search_strategy/{search_strategy_registry.test.js => search_strategy_registry.test.ts} (58%) rename src/legacy/ui/public/courier/search_strategy/{search_strategy_registry.js => search_strategy_registry.ts} (64%) create mode 100644 src/legacy/ui/public/courier/search_strategy/types.ts rename src/legacy/ui/public/courier/{search_strategy/index.d.ts => types.ts} (84%) delete mode 100644 src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts rename src/legacy/ui/public/courier/utils/{courier_inspector_utils.js => courier_inspector_utils.ts} (78%) rename src/legacy/ui/public/courier/{index.js => utils/types.ts} (71%) delete mode 100644 src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js create mode 100644 src/legacy/ui/public/field_wildcard/field_wildcard.test.ts rename src/legacy/ui/public/field_wildcard/{field_wildcard.js => field_wildcard.ts} (70%) rename src/legacy/ui/public/field_wildcard/{index.js => index.ts} (100%) diff --git a/src/fixtures/stubbed_search_source.js b/src/fixtures/stubbed_search_source.js index 3a36b97e6757e..da741a1aa4774 100644 --- a/src/fixtures/stubbed_search_source.js +++ b/src/fixtures/stubbed_search_source.js @@ -60,9 +60,6 @@ export default function stubSearchSource(Private, $q, Promise) { onRequestStart(fn) { this._requestStartHandlers.push(fn); }, - requestIsStarting(req) { - return Promise.map(this._requestStartHandlers, fn => fn(req)); - }, requestIsStopped() {} }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index db2a803ea1c61..7165de026920d 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -29,7 +29,12 @@ import { ExpressionFunction, KibanaDatatableColumn, } from 'src/plugins/expressions/public'; -import { SearchSource } from '../../../../../ui/public/courier/search_source'; +import { + SearchSource, + SearchSourceContract, + getRequestInspectorStats, + getResponseInspectorStats, +} from '../../../../../ui/public/courier'; // @ts-ignore import { FilterBarQueryFilterProvider, @@ -37,10 +42,6 @@ import { } from '../../../../../ui/public/filter_manager/query_filter'; import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../../../../ui/public/courier/utils/courier_inspector_utils'; import { calculateObjectHash } from '../../../../../ui/public/vis/lib/calculate_object_hash'; import { getTime } from '../../../../../ui/public/timefilter'; // @ts-ignore @@ -50,7 +51,7 @@ import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; export interface RequestHandlerParams { - searchSource: SearchSource; + searchSource: SearchSourceContract; aggs: AggConfigs; timeRange?: TimeRange; query?: Query; @@ -119,7 +120,7 @@ const handleCourierRequest = async ({ return aggs.toDsl(metricsAtAllLevels); }); - requestSearchSource.onRequestStart((paramSearchSource: SearchSource, options: any) => { + requestSearchSource.onRequestStart((paramSearchSource, options) => { return aggs.onSearchRequestStart(paramSearchSource, options); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js index 61a3d4084ab8f..2ab4131957c32 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js @@ -19,9 +19,9 @@ import { timefilter } from 'ui/timefilter'; export function createSearchSource(SearchSource, initialState, indexPattern, aggs, useTimeFilter, filters = []) { - const searchSource = new SearchSource(initialState); + const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals - searchSource.setParent(false); + searchSource.setParent(undefined); searchSource.setField('filter', () => { const activeFilters = [...filters]; if (useTimeFilter) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 5b24aa13f4f77..4c417ed2954d3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; export interface SavedObjectDashboard extends SavedObject { @@ -34,7 +34,7 @@ export interface SavedObjectDashboard extends SavedObject { // TODO: write a migration to rid of this, it's only around for bwc. uiStateJSON?: string; lastSavedTitle: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; destroy: () => void; refreshInterval?: RefreshInterval; getQuery(): Query; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js index 46e66177b516a..4eb68c1bf50bc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js @@ -58,7 +58,7 @@ describe('context app', function () { .then(() => { const setParentSpy = searchSourceStub.setParent; expect(setParentSpy.calledOnce).to.be(true); - expect(setParentSpy.firstCall.args[0]).to.eql(false); + expect(setParentSpy.firstCall.args[0]).to.be(undefined); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js index 2bf3da42e24e5..ea6a8c092e242 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js @@ -196,7 +196,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js index b8bec40f2859c..486c8ed9b410e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js @@ -199,7 +199,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js index 62bbc6166662f..8c4cce810ca13 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js @@ -30,7 +30,7 @@ export function fetchAnchorProvider(indexPatterns) { ) { const indexPattern = await indexPatterns.get(indexPatternId); const searchSource = new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('version', true) .setField('size', 1) diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts index 3314bbbf189c4..68ccf56594e72 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts @@ -17,8 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../ui/public/courier'; import { IndexPatterns, IndexPattern, getServices } from '../../../kibana_services'; -import { reverseSortDir, SortDirection } from './utils/sorting'; +import { reverseSortDir } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; import { generateIntervals } from './utils/generate_intervals'; @@ -114,7 +115,7 @@ function fetchContextProvider(indexPatterns: IndexPatterns) { async function createSearchSource(indexPattern: IndexPattern, filters: esFilters.Filter[]) { return new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('filter', filters); } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts index eeae2aa2c5d0a..33f4454c18d40 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { reverseSortDir, SortDirection } from '../sorting'; +import { reverseSortDir } from '../sorting'; +import { SortDirection } from '../../../../../../../../../ui/public/courier'; + +jest.mock('ui/new_platform'); describe('function reverseSortDir', function() { test('reverse a given sort direction', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts index 2810e5d9d7e66..19c2ee2cdfe10 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { SearchSource } from '../../../../kibana_services'; +import { + EsQuerySortValue, + SortDirection, + SearchSourceContract, +} from '../../../../../../../../ui/public/courier'; import { convertTimeValueToIso } from './date_conversion'; -import { SortDirection } from './sorting'; import { EsHitRecordList } from '../context'; import { IntervalValue } from './generate_intervals'; -import { EsQuerySort } from './get_es_query_sort'; import { EsQuerySearchAfter } from './get_es_query_search_after'; interface RangeQuery { @@ -38,9 +40,9 @@ interface RangeQuery { * and filters set. */ export async function fetchHitsInInterval( - searchSource: SearchSource, + searchSource: SearchSourceContract, timeField: string, - sort: EsQuerySort, + sort: [EsQuerySortValue, EsQuerySortValue], sortDir: SortDirection, interval: IntervalValue[], searchAfter: EsQuerySearchAfter, diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts index a50764fe542b1..cb4878239ff92 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; +import { SortDirection } from '../../../../../../../../ui/public/courier'; export type IntervalValue = number | null; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts index c9f9b9b939f3d..39c69112e58cb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts @@ -16,11 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; -type EsQuerySortValue = Record; - -export type EsQuerySort = [EsQuerySortValue, EsQuerySortValue]; +import { EsQuerySortValue, SortDirection } from '../../../../../../../../ui/public/courier/types'; /** * Returns `EsQuerySort` which is used to sort records in the ES query @@ -33,6 +30,6 @@ export function getEsQuerySort( timeField: string, tieBreakerField: string, sortDir: SortDirection -): EsQuerySort { +): [EsQuerySortValue, EsQuerySortValue] { return [{ [timeField]: sortDir }, { [tieBreakerField]: sortDir }]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts index 4a0f531845f46..47385aecb1937 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts @@ -17,13 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../../ui/public/courier'; import { IndexPattern } from '../../../../kibana_services'; -export enum SortDirection { - asc = 'asc', - desc = 'desc', -} - /** * The list of field names that are allowed for sorting, but not included in * index pattern fields. diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index ef79cda476e51..9fee0cfc3ea00 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, TimeRange, @@ -51,7 +52,6 @@ import { getServices, IndexPattern, RequestAdapter, - SearchSource, } from '../kibana_services'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; @@ -92,7 +92,7 @@ export class SearchEmbeddable extends Embeddable private inspectorAdaptors: Adapters; private searchScope?: SearchScope; private panelTitle: string = ''; - private filtersSearchSource?: SearchSource; + private filtersSearchSource?: SearchSourceContract; private searchInstance?: JQLite; private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; @@ -194,13 +194,11 @@ export class SearchEmbeddable extends Embeddable searchScope.inspectorAdapters = this.inspectorAdaptors; const { searchSource } = this.savedSearch; - const indexPattern = (searchScope.indexPattern = searchSource.getField('index')); + const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; const timeRangeSearchSource = searchSource.create(); timeRangeSearchSource.setField('filter', () => { - if (!this.searchScope || !this.input.timeRange) { - return; - } + if (!this.searchScope || !this.input.timeRange) return; return getTime(indexPattern, this.input.timeRange); }); @@ -241,7 +239,7 @@ export class SearchEmbeddable extends Embeddable }; searchScope.filter = async (field, value, operator) => { - let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id); + let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id!); filters = filters.map(filter => ({ ...filter, $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index 1939cc7060621..ebea646a09889 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -84,6 +84,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< const queryFilter = Private(getServices().FilterBarQueryFilterProvider); try { const savedObject = await searchLoader.get(savedObjectId); + const indexPattern = savedObject.searchSource.getField('index'); return new SearchEmbeddable( { savedSearch: savedObject, @@ -92,7 +93,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< editUrl, queryFilter, editable: getServices().capabilities.discover.save as boolean, - indexPatterns: _.compact([savedObject.searchSource.getField('index')]), + indexPatterns: indexPattern ? [indexPattern] : [], }, input, this.executeTriggerActions, 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 02b08d7fa4b61..fc5f34fab7564 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -28,7 +28,6 @@ import angular from 'angular'; // just used in embeddables and discover controll import uiRoutes from 'ui/routes'; // @ts-ignore import { uiModules } from 'ui/modules'; -import { SearchSource } from 'ui/courier'; // @ts-ignore import { StateProvider } from 'ui/state_management/state'; // @ts-ignore @@ -43,6 +42,7 @@ import { wrapInI18nContext } from 'ui/i18n'; import { docTitle } from 'ui/doc_title'; // @ts-ignore import * as docViewsRegistry from 'ui/registry/doc_views'; +import { SearchSource } from '../../../../ui/public/courier'; const services = { // new plattform @@ -87,9 +87,10 @@ export { callAfterBindingsWorkaround } from 'ui/compat'; export { getRequestInspectorStats, getResponseInspectorStats, -} from 'ui/courier/utils/courier_inspector_utils'; -// @ts-ignore -export { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; + hasSearchStategyForIndexPattern, + isDefaultTypeIndexPattern, + SearchSource, +} from '../../../../ui/public/courier'; // @ts-ignore export { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; // @ts-ignore @@ -115,7 +116,6 @@ export { unhashUrl } from 'ui/state_management/state_hashing'; // EXPORT types export { Vis } from 'ui/vis'; export { StaticIndexPattern, IndexPatterns, IndexPattern, FieldType } from 'ui/index_patterns'; -export { SearchSource } from 'ui/courier'; export { ElasticSearchHit } from 'ui/registry/doc_views_types'; export { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; export { Adapters } from 'ui/inspector/types'; diff --git a/src/legacy/core_plugins/kibana/public/discover/types.d.ts b/src/legacy/core_plugins/kibana/public/discover/types.d.ts index 7d8740243ec02..6cdd802fa2800 100644 --- a/src/legacy/core_plugins/kibana/public/discover/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/types.d.ts @@ -17,13 +17,14 @@ * under the License. */ -import { SearchSource } from './kibana_services'; +import { SearchSourceContract } from '../../../../ui/public/courier'; import { SortOrder } from './angular/doc_table/components/table_header/helpers'; +export { SortOrder } from './angular/doc_table/components/table_header/helpers'; export interface SavedSearch { readonly id: string; title: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; description?: string; columns: string[]; sort: SortOrder[]; diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 924f72594ad34..a2b46dab1ef33 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -25,12 +25,12 @@ import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/saved_object'; import { Vis } from 'ui/vis'; -import { SearchSource } from 'ui/courier'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { TimeRange, @@ -53,7 +53,7 @@ const getKeys = (o: T): Array => Object.keys(o) as Array< export interface VisSavedObject extends SavedObject { vis: Vis; description?: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; title: string; uiStateJSON?: string; destroy: () => void; diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index de1a6059774e7..d4ef203721456 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -27,6 +27,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; import { AggType } from './agg_type'; import { FieldParamType } from './param_types/field'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; @@ -233,10 +234,10 @@ export class AggConfig { /** * Hook for pre-flight logic, see AggType#onSearchRequestStart * @param {Courier.SearchSource} searchSource - * @param {Courier.SearchRequest} searchRequest + * @param {Courier.FetchOptions} options * @return {Promise} */ - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { if (!this.type) { return Promise.resolve(); } diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 7c0245f30a1fd..2f6951891f84d 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -33,6 +33,7 @@ import { Schema } from '../vis/editors/default/schemas'; import { AggConfig, AggConfigOptions } from './agg_config'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { IndexPattern } from '../../../core_plugins/data/public'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; function removeParentAggs(obj: any) { for (const prop in obj) { @@ -301,7 +302,7 @@ export class AggConfigs { return _.find(reqAgg.getResponseAggs(), { id }); } - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { return Promise.all( // @ts-ignore this.getRequestAggs().map((agg: AggConfig) => agg.onSearchRequestStart(searchSource, options)) diff --git a/src/legacy/ui/public/agg_types/buckets/terms.ts b/src/legacy/ui/public/agg_types/buckets/terms.ts index 89e33784fb5fb..6ce0b9ce38ad3 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/terms.ts @@ -19,16 +19,12 @@ import chrome from 'ui/chrome'; import { noop } from 'lodash'; -import { SearchSource } from 'ui/courier'; import { i18n } from '@kbn/i18n'; +import { SearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../../courier'; import { BucketAggType, BucketAggParam } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { AggConfigOptions } from '../agg_config'; import { IBucketAggConfig } from './_bucket_agg_type'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../courier/utils/courier_inspector_utils'; import { createFilterTerms } from './create_filter/terms'; import { wrapWithInlineComp } from './inline_comp_wrapper'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; diff --git a/src/legacy/ui/public/agg_types/param_types/base.ts b/src/legacy/ui/public/agg_types/param_types/base.ts index bc8ed5d485bd4..61ef73fb62e8a 100644 --- a/src/legacy/ui/public/agg_types/param_types/base.ts +++ b/src/legacy/ui/public/agg_types/param_types/base.ts @@ -20,7 +20,7 @@ import { AggParam } from '../'; import { AggConfigs } from '../agg_configs'; import { AggConfig } from '../../vis'; -import { SearchSource } from '../../courier'; +import { SearchSourceContract, FetchOptions } from '../../courier/types'; export class BaseParamType implements AggParam { name: string; @@ -55,8 +55,8 @@ export class BaseParamType implements AggParam { */ modifyAggConfigOnSearchRequestStart: ( aggConfig: AggConfig, - searchSource?: SearchSource, - options?: any + searchSource?: SearchSourceContract, + options?: FetchOptions ) => void; constructor(config: Record) { diff --git a/src/legacy/ui/public/courier/fetch/call_client.test.js b/src/legacy/ui/public/courier/fetch/call_client.test.ts similarity index 64% rename from src/legacy/ui/public/courier/fetch/call_client.test.js rename to src/legacy/ui/public/courier/fetch/call_client.test.ts index 463d6c59e479e..74c87d77dd4fd 100644 --- a/src/legacy/ui/public/courier/fetch/call_client.test.js +++ b/src/legacy/ui/public/courier/fetch/call_client.test.ts @@ -19,61 +19,64 @@ import { callClient } from './call_client'; import { handleResponse } from './handle_response'; +import { FetchHandlers, SearchRequest, SearchStrategySearchParams } from '../types'; const mockResponses = [{}, {}]; const mockAbortFns = [jest.fn(), jest.fn()]; const mockSearchFns = [ - jest.fn(({ searchRequests }) => ({ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), - abort: mockAbortFns[0] + abort: mockAbortFns[0], })), - jest.fn(({ searchRequests }) => ({ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), - abort: mockAbortFns[1] - })) + abort: mockAbortFns[1], + })), ]; const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); jest.mock('./handle_response', () => ({ - handleResponse: jest.fn((request, response) => response) + handleResponse: jest.fn((request, response) => response), })); jest.mock('../search_strategy', () => ({ - getSearchStrategyForSearchRequest: request => mockSearchStrategies[request._searchStrategyId], - getSearchStrategyById: id => mockSearchStrategies[id] + getSearchStrategyForSearchRequest: (request: SearchRequest) => + mockSearchStrategies[request._searchStrategyId], + getSearchStrategyById: (id: number) => mockSearchStrategies[id], })); describe('callClient', () => { beforeEach(() => { - handleResponse.mockClear(); + (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); + 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[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]]); + 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', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }]; - const args = { es: {}, config: {}, esShardTimeout: 0 }; + const searchRequests = [{ _searchStrategyId: 0 }]; + const args = { es: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); @@ -82,25 +85,17 @@ describe('callClient', () => { }); test('Returns the responses in the original order', async () => { - const searchRequests = [{ - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }]; + const searchRequests = [{ _searchStrategyId: 1 }, { _searchStrategyId: 0 }]; - const responses = await Promise.all(callClient(searchRequests)); + const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); expect(responses).toEqual([mockResponses[1], mockResponses[0]]); }); test('Calls handleResponse with each request and response', async () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; - const responses = callClient(searchRequests); + const responses = callClient(searchRequests, [], {} as FetchHandlers); await Promise.all(responses); expect(handleResponse).toBeCalledTimes(2); @@ -109,17 +104,15 @@ describe('callClient', () => { }); test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; const abortController = new AbortController(); - const requestOptions = [{ - abortSignal: abortController.signal - }]; + const requestOptions = [ + { + abortSignal: abortController.signal, + }, + ]; - callClient(searchRequests, requestOptions); + callClient(searchRequests, requestOptions, {} as FetchHandlers); abortController.abort(); expect(mockAbortFns[0]).toBeCalled(); diff --git a/src/legacy/ui/public/courier/fetch/call_client.js b/src/legacy/ui/public/courier/fetch/call_client.ts similarity index 70% rename from src/legacy/ui/public/courier/fetch/call_client.js rename to src/legacy/ui/public/courier/fetch/call_client.ts index 971ae4c49a604..43da27f941e4e 100644 --- a/src/legacy/ui/public/courier/fetch/call_client.js +++ b/src/legacy/ui/public/courier/fetch/call_client.ts @@ -20,11 +20,20 @@ import { groupBy } from 'lodash'; import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; import { handleResponse } from './handle_response'; +import { FetchOptions, FetchHandlers } from './types'; +import { SearchRequest } from '../types'; -export function callClient(searchRequests, requestsOptions = [], { es, config, esShardTimeout } = {}) { +export function callClient( + searchRequests: SearchRequest[], + requestsOptions: FetchOptions[] = [], + { es, config, esShardTimeout }: FetchHandlers +) { // Correlate the options with the request that they're associated with - const requestOptionEntries = searchRequests.map((request, i) => [request, requestsOptions[i]]); - const requestOptionsMap = new Map(requestOptionEntries); + const requestOptionEntries: Array<[ + SearchRequest, + 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) => { @@ -39,15 +48,22 @@ export function callClient(searchRequests, requestsOptions = [], { es, config, e Object.keys(searchStrategyMap).forEach(searchStrategyId => { const searchStrategy = getSearchStrategyById(searchStrategyId); const requests = searchStrategyMap[searchStrategyId]; - const { searching, abort } = searchStrategy.search({ searchRequests: requests, es, config, esShardTimeout }); + + // 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, + es, + config, + esShardTimeout, + }); + requests.forEach((request, i) => { const response = searching.then(results => handleResponse(request, results[i])); - const { abortSignal } = requestOptionsMap.get(request) || {}; + 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/legacy/ui/public/courier/fetch/components/shard_failure_types.ts b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts index de32b9d7b3087..22fc20233cc87 100644 --- a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts +++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts @@ -24,6 +24,7 @@ export interface Request { sort: unknown; stored_fields: string[]; } + export interface ResponseWithShardFailure { _shards: { failed: number; diff --git a/src/legacy/ui/public/courier/fetch/errors.ts b/src/legacy/ui/public/courier/fetch/errors.ts index aba554a795258..a2ac013915b4b 100644 --- a/src/legacy/ui/public/courier/fetch/errors.ts +++ b/src/legacy/ui/public/courier/fetch/errors.ts @@ -17,17 +17,18 @@ * under the License. */ +import { SearchError } from '../../courier'; import { KbnError } from '../../../../../plugins/kibana_utils/public'; +import { SearchResponse } from '../types'; /** * Request Failure - When an entire multi request fails * @param {Error} err - the Error that came back * @param {Object} resp - optional HTTP response */ export class RequestFailure extends KbnError { - public resp: any; - constructor(err: any, resp?: any) { - err = err || false; - super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err.message)}`); + public resp: SearchResponse; + constructor(err: SearchError | null = null, resp?: SearchResponse) { + super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`); this.resp = resp; } diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.js b/src/legacy/ui/public/courier/fetch/fetch_soon.js deleted file mode 100644 index ef02beddcb59a..0000000000000 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.js +++ /dev/null @@ -1,70 +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 { callClient } from './call_client'; - -/** - * This function introduces a slight delay in the request process to allow multiple requests to queue - * up (e.g. when a dashboard is loading). - */ -export async function fetchSoon(request, options, { es, config, esShardTimeout }) { - const delay = config.get('courier:batchSearches') ? 50 : 0; - return delayedFetch(request, options, { es, config, esShardTimeout }, delay); -} - -/** - * Delays executing a function for a given amount of time, and returns a promise that resolves - * with the result. - * @param fn The function to invoke - * @param ms The number of milliseconds to wait - * @return Promise A promise that resolves with the result of executing the function - */ -function delay(fn, ms) { - return new Promise(resolve => { - setTimeout(() => resolve(fn()), ms); - }); -} - -// The current batch/queue of requests to fetch -let requestsToFetch = []; -let requestOptions = []; - -// The in-progress fetch (if there is one) -let fetchInProgress = null; - -/** - * Delay fetching for a given amount of time, while batching up the requests to be fetched. - * Returns a promise that resolves with the response for the given request. - * @param request The request to fetch - * @param ms The number of milliseconds to wait (and batch requests) - * @return Promise The response for the given request - */ -async function delayedFetch(request, options, { es, config, esShardTimeout }, ms) { - const i = requestsToFetch.length; - requestsToFetch = [...requestsToFetch, request]; - requestOptions = [...requestOptions, options]; - const responses = await (fetchInProgress = fetchInProgress || delay(() => { - const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); - requestsToFetch = []; - requestOptions = []; - fetchInProgress = null; - return response; - }, ms)); - return responses[i]; -} diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts similarity index 63% rename from src/legacy/ui/public/courier/fetch/fetch_soon.test.js rename to src/legacy/ui/public/courier/fetch/fetch_soon.test.ts index 824a4ab7e12e3..e753c526b748d 100644 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts @@ -19,47 +19,53 @@ import { fetchSoon } from './fetch_soon'; import { callClient } from './call_client'; - -function getMockConfig(config) { - const entries = Object.entries(config); - return new Map(entries); +import { UiSettingsClientContract } from '../../../../../core/public'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +function getConfigStub(config: any = {}) { + return { + get: key => config[key], + } as UiSettingsClientContract; } -const mockResponses = { - 'foo': {}, - 'bar': {}, - 'baz': {}, +const mockResponses: Record = { + foo: {}, + bar: {}, + baz: {}, }; jest.useFakeTimers(); jest.mock('./call_client', () => ({ - callClient: jest.fn(requests => { + callClient: jest.fn((requests: SearchRequest[]) => { // Allow a request object to specify which mockResponse it wants to receive (_mockResponseId) // in addition to how long to simulate waiting before returning a response (_waitMs) const responses = requests.map(request => { - const waitMs = requests.reduce((total, request) => request._waitMs || 0, 0); + const waitMs = requests.reduce((total, { _waitMs }) => total + _waitMs || 0, 0); return new Promise(resolve => { - resolve(mockResponses[request._mockResponseId]); - }, waitMs); + setTimeout(() => { + resolve(mockResponses[request._mockResponseId]); + }, waitMs); + }); }); return Promise.resolve(responses); - }) + }), })); describe('fetchSoon', () => { beforeEach(() => { - callClient.mockClear(); + (callClient as jest.Mock).mockClear(); }); test('should delay by 0ms if config is set to not batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': false + const config = getConfigStub({ + 'courier:batchSearches': false, }); const request = {}; const options = {}; - fetchSoon(request, options, { config }); + fetchSoon(request, options, { config } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -67,13 +73,13 @@ describe('fetchSoon', () => { }); test('should delay by 50ms if config is set to batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const request = {}; const options = {}; - fetchSoon(request, options, { config }); + fetchSoon(request, options, { config } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -83,30 +89,30 @@ describe('fetchSoon', () => { }); test('should send a batch of requests to callClient', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const requests = [{ foo: 1 }, { foo: 2 }]; const options = [{ bar: 1 }, { bar: 2 }]; requests.forEach((request, i) => { - fetchSoon(request, options[i], { config }); + fetchSoon(request, options[i] as FetchOptions, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(requests); - expect(callClient.mock.calls[0][1]).toEqual(options); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(requests); + expect((callClient as jest.Mock).mock.calls[0][1]).toEqual(options); }); test('should return the response to the corresponding call for multiple batched requests', async () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; const promises = requests.map(request => { - return fetchSoon(request, {}, { config }); + return fetchSoon(request, {}, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); const results = await Promise.all(promises); @@ -115,26 +121,26 @@ describe('fetchSoon', () => { }); test('should wait for the previous batch to start before starting a new batch', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const firstBatch = [{ foo: 1 }, { foo: 2 }]; const secondBatch = [{ bar: 1 }, { bar: 2 }]; firstBatch.forEach(request => { - fetchSoon(request, {}, { config }); + fetchSoon(request, {}, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); secondBatch.forEach(request => { - fetchSoon(request, {}, { config }); + fetchSoon(request, {}, { config } as FetchHandlers); }); expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(firstBatch); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(firstBatch); jest.advanceTimersByTime(50); expect(callClient).toBeCalledTimes(2); - expect(callClient.mock.calls[1][0]).toEqual(secondBatch); + expect((callClient as jest.Mock).mock.calls[1][0]).toEqual(secondBatch); }); }); diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.ts b/src/legacy/ui/public/courier/fetch/fetch_soon.ts new file mode 100644 index 0000000000000..75de85e02a1a2 --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { callClient } from './call_client'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +/** + * This function introduces a slight delay in the request process to allow multiple requests to queue + * up (e.g. when a dashboard is loading). + */ +export async function fetchSoon( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers +) { + const msToDelay = config.get('courier:batchSearches') ? 50 : 0; + return delayedFetch(request, options, { es, config, esShardTimeout }, msToDelay); +} + +/** + * Delays executing a function for a given amount of time, and returns a promise that resolves + * with the result. + * @param fn The function to invoke + * @param ms The number of milliseconds to wait + * @return Promise A promise that resolves with the result of executing the function + */ +function delay(fn: Function, ms: number) { + return new Promise(resolve => { + setTimeout(() => resolve(fn()), ms); + }); +} + +// The current batch/queue of requests to fetch +let requestsToFetch: SearchRequest[] = []; +let requestOptions: FetchOptions[] = []; + +// The in-progress fetch (if there is one) +let fetchInProgress: Promise | null = null; + +/** + * Delay fetching for a given amount of time, while batching up the requests to be fetched. + * Returns a promise that resolves with the response for the given request. + * @param request The request to fetch + * @param ms The number of milliseconds to wait (and batch requests) + * @return Promise The response for the given request + */ +async function delayedFetch( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers, + ms: number +) { + const i = requestsToFetch.length; + requestsToFetch = [...requestsToFetch, request]; + requestOptions = [...requestOptions, options]; + const responses = await (fetchInProgress = + fetchInProgress || + delay(() => { + const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); + requestsToFetch = []; + requestOptions = []; + fetchInProgress = null; + return response; + }, ms)); + return responses[i]; +} diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.test.js b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts similarity index 96% rename from src/legacy/ui/public/courier/fetch/get_search_params.test.js rename to src/legacy/ui/public/courier/fetch/get_search_params.test.ts index 380d1da963ddf..d6f3d33099599 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.test.js +++ b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts @@ -18,11 +18,12 @@ */ import { getMSearchParams, getSearchParams } from './get_search_params'; +import { UiSettingsClientContract } from '../../../../../core/public'; -function getConfigStub(config = {}) { +function getConfigStub(config: any = {}) { return { - get: key => config[key] - }; + get: key => config[key], + } as UiSettingsClientContract; } describe('getMSearchParams', () => { diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.js b/src/legacy/ui/public/courier/fetch/get_search_params.ts similarity index 73% rename from src/legacy/ui/public/courier/fetch/get_search_params.js rename to src/legacy/ui/public/courier/fetch/get_search_params.ts index dd55201ba5540..6b8da07ca93d4 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.js +++ b/src/legacy/ui/public/courier/fetch/get_search_params.ts @@ -17,9 +17,11 @@ * under the License. */ +import { UiSettingsClientContract } from '../../../../../core/public'; + const sessionId = Date.now(); -export function getMSearchParams(config) { +export function getMSearchParams(config: UiSettingsClientContract) { return { rest_total_hits_as_int: true, ignore_throttled: getIgnoreThrottled(config), @@ -27,7 +29,7 @@ export function getMSearchParams(config) { }; } -export function getSearchParams(config, esShardTimeout) { +export function getSearchParams(config: UiSettingsClientContract, esShardTimeout: number = 0) { return { rest_total_hits_as_int: true, ignore_unavailable: true, @@ -38,21 +40,23 @@ export function getSearchParams(config, esShardTimeout) { }; } -export function getIgnoreThrottled(config) { +export function getIgnoreThrottled(config: UiSettingsClientContract) { return !config.get('search:includeFrozen'); } -export function getMaxConcurrentShardRequests(config) { +export function getMaxConcurrentShardRequests(config: UiSettingsClientContract) { const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; } -export function getPreference(config) { +export function getPreference(config: UiSettingsClientContract) { const setRequestPreference = config.get('courier:setRequestPreference'); if (setRequestPreference === 'sessionId') return sessionId; - return setRequestPreference === 'custom' ? config.get('courier:customRequestPreference') : undefined; + return setRequestPreference === 'custom' + ? config.get('courier:customRequestPreference') + : undefined; } -export function getTimeout(esShardTimeout) { +export function getTimeout(esShardTimeout: number) { return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined; } diff --git a/src/legacy/ui/public/courier/fetch/handle_response.test.js b/src/legacy/ui/public/courier/fetch/handle_response.test.ts similarity index 78% rename from src/legacy/ui/public/courier/fetch/handle_response.test.js rename to src/legacy/ui/public/courier/fetch/handle_response.test.ts index 0836832e6c05a..0163aca777161 100644 --- a/src/legacy/ui/public/courier/fetch/handle_response.test.js +++ b/src/legacy/ui/public/courier/fetch/handle_response.test.ts @@ -23,46 +23,50 @@ import { toastNotifications } from '../../notify/toasts'; jest.mock('../../notify/toasts', () => { return { toastNotifications: { - addWarning: jest.fn() - } + addWarning: jest.fn(), + }, }; }); jest.mock('@kbn/i18n', () => { return { i18n: { - translate: (id, { defaultMessage }) => defaultMessage - } + translate: (id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage, + }, }; }); describe('handleResponse', () => { beforeEach(() => { - toastNotifications.addWarning.mockReset(); + (toastNotifications.addWarning as jest.Mock).mockReset(); }); test('should notify if timed out', () => { const request = { body: {} }; const response = { - timed_out: true + timed_out: true, }; const result = handleResponse(request, response); expect(result).toBe(response); expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('request timed out'); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'request timed out' + ); }); test('should notify if shards failed', () => { const request = { body: {} }; const response = { _shards: { - failed: true - } + failed: true, + }, }; const result = handleResponse(request, response); expect(result).toBe(response); expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('shards failed'); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'shards failed' + ); }); test('returns the response', () => { diff --git a/src/legacy/ui/public/courier/fetch/handle_response.js b/src/legacy/ui/public/courier/fetch/handle_response.tsx similarity index 71% rename from src/legacy/ui/public/courier/fetch/handle_response.js rename to src/legacy/ui/public/courier/fetch/handle_response.tsx index fb2797369d78f..d7f2263268f8c 100644 --- a/src/legacy/ui/public/courier/fetch/handle_response.js +++ b/src/legacy/ui/public/courier/fetch/handle_response.tsx @@ -17,14 +17,16 @@ * under the License. */ - import React from 'react'; -import { toastNotifications } from '../../notify/toasts'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; +import { toastNotifications } from '../../notify/toasts'; import { ShardFailureOpenModalButton } from './components/shard_failure_open_modal_button'; +import { Request, ResponseWithShardFailure } from './components/shard_failure_types'; +import { SearchRequest, SearchResponse } from '../types'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; -export function handleResponse(request, response) { +export function handleResponse(request: SearchRequest, response: SearchResponse) { if (response.timed_out) { toastNotifications.addWarning({ title: i18n.translate('common.ui.courier.fetch.requestTimedOutNotificationMessage', { @@ -41,26 +43,26 @@ export function handleResponse(request, response) { shardsTotal: response._shards.total, }, }); - const description = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationDescription', { - defaultMessage: 'The data you are seeing might be incomplete or wrong.', - }); + const description = i18n.translate( + 'common.ui.courier.fetch.shardsFailedNotificationDescription', + { + defaultMessage: 'The data you are seeing might be incomplete or wrong.', + } + ); - const text = ( + const text = toMountPoint( <> {description} - + ); - toastNotifications.addWarning({ - title, - text, - }); + toastNotifications.addWarning({ title, text }); } return response; diff --git a/src/legacy/ui/public/courier/fetch/index.js b/src/legacy/ui/public/courier/fetch/index.ts similarity index 100% rename from src/legacy/ui/public/courier/fetch/index.js rename to src/legacy/ui/public/courier/fetch/index.ts diff --git a/src/legacy/ui/public/courier/fetch/types.ts b/src/legacy/ui/public/courier/fetch/types.ts new file mode 100644 index 0000000000000..e341e1ab35c5c --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/types.ts @@ -0,0 +1,41 @@ +/* + * 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 { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface ApiCaller { + search: (searchRequest: SearchRequest) => ApiCallerResponse; + msearch: (searchRequest: SearchRequest) => ApiCallerResponse; +} + +export interface ApiCallerResponse extends Promise { + abort: () => void; +} + +export interface FetchOptions { + abortSignal?: AbortSignal; + searchStrategyId?: string; +} + +export interface FetchHandlers { + es: ApiCaller; + config: UiSettingsClientContract; + esShardTimeout: number; +} diff --git a/src/legacy/ui/public/courier/index.d.ts b/src/legacy/ui/public/courier/index.ts similarity index 94% rename from src/legacy/ui/public/courier/index.d.ts rename to src/legacy/ui/public/courier/index.ts index 93556c2666c9a..3c16926d2aba7 100644 --- a/src/legacy/ui/public/courier/index.d.ts +++ b/src/legacy/ui/public/courier/index.ts @@ -17,6 +17,8 @@ * under the License. */ +export * from './fetch'; export * from './search_source'; export * from './search_strategy'; export * from './utils/courier_inspector_utils'; +export * from './types'; diff --git a/src/legacy/ui/public/courier/search_poll/search_poll.js b/src/legacy/ui/public/courier/search_poll/search_poll.js deleted file mode 100644 index f00c2a32e0ec6..0000000000000 --- a/src/legacy/ui/public/courier/search_poll/search_poll.js +++ /dev/null @@ -1,68 +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 _ from 'lodash'; - -import { timefilter } from 'ui/timefilter'; - -export class SearchPoll { - constructor() { - this._isPolling = false; - this._intervalInMs = undefined; - this._timerId = null; - } - - setIntervalInMs = intervalInMs => { - this._intervalInMs = _.parseInt(intervalInMs); - }; - - resume = () => { - this._isPolling = true; - this.resetTimer(); - }; - - pause = () => { - this._isPolling = false; - this.clearTimer(); - }; - - resetTimer = () => { - // Cancel the pending search and schedule a new one. - this.clearTimer(); - - if (this._isPolling) { - this._timerId = setTimeout(this._search, this._intervalInMs); - } - }; - - clearTimer = () => { - // Cancel the pending search, if there is one. - if (this._timerId) { - clearTimeout(this._timerId); - this._timerId = null; - } - }; - - _search = () => { - // Schedule another search. - this.resetTimer(); - - timefilter.notifyShouldFetch(); - }; -} diff --git a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js deleted file mode 100644 index 279e389dec114..0000000000000 --- a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js +++ /dev/null @@ -1,124 +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 '../../../private'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import { normalizeSortRequest } from '../_normalize_sort_request'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import _ from 'lodash'; - -describe('SearchSource#normalizeSortRequest', function () { - let indexPattern; - let normalizedSort; - const defaultSortOptions = { unmapped_type: 'boolean' }; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - normalizedSort = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - })); - - it('should return an array', function () { - const sortable = { someField: 'desc' }; - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(result).to.be.an(Array); - expect(result).to.eql(normalizedSort); - // ensure object passed in is not mutated - expect(result[0]).to.not.be.equal(sortable); - expect(sortable).to.eql({ someField: 'desc' }); - }); - - it('should make plain string sort into the more verbose format', function () { - const result = normalizeSortRequest([{ someField: 'desc' }], indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should append default sort options', function () { - const sortState = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - const result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should enable script based sorting', function () { - const fieldName = 'script string'; - const direction = 'desc'; - const indexField = indexPattern.fields.getByName(fieldName); - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = { - _script: { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: indexField.type, - order: direction - } - }; - - let result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - - sortState[fieldName] = { order: direction }; - result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - }); - - it('should use script based sorting only on sortable types', function () { - const fieldName = 'script murmur3'; - const direction = 'asc'; - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = {}; - normalizedSort[fieldName] = { - order: direction, - unmapped_type: 'boolean' - }; - const result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - - expect(result).to.eql([normalizedSort]); - }); - - it('should remove unmapped_type parameter from _score sorting', function () { - const sortable = { _score: 'desc' }; - const expected = [{ - _score: { - order: 'desc' - } - }]; - - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(_.isEqual(result, expected)).to.be.ok(); - - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js deleted file mode 100644 index 3e5d7a1374115..0000000000000 --- a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js +++ /dev/null @@ -1,88 +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 _ from 'lodash'; - -/** - * Decorate queries with default parameters - * @param {query} query object - * @returns {object} - */ -export function normalizeSortRequest(sortObject, indexPattern, defaultSortOptions) { - // [].concat({}) -> [{}], [].concat([{}]) -> [{}] - return [].concat(sortObject).map(function (sortable) { - return normalize(sortable, indexPattern, defaultSortOptions); - }); -} - -/* - Normalize the sort description to the more verbose format: - { someField: "desc" } into { someField: { "order": "desc"}} - */ -function normalize(sortable, indexPattern, defaultSortOptions) { - const normalized = {}; - let sortField = _.keys(sortable)[0]; - let sortValue = sortable[sortField]; - const indexField = indexPattern.fields.getByName(sortField); - - if (indexField && indexField.scripted && indexField.sortable) { - let direction; - if (_.isString(sortValue)) direction = sortValue; - if (_.isObject(sortValue) && sortValue.order) direction = sortValue.order; - - sortField = '_script'; - sortValue = { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: castSortType(indexField.type), - order: direction - }; - } else { - if (_.isString(sortValue)) { - sortValue = { order: sortValue }; - } - sortValue = _.defaults({}, sortValue, defaultSortOptions); - - if (sortField === '_score') { - delete sortValue.unmapped_type; - } - } - - normalized[sortField] = sortValue; - return normalized; -} - -// The ES API only supports sort scripts of type 'number' and 'string' -function castSortType(type) { - const typeCastings = { - number: 'number', - string: 'string', - date: 'number', - boolean: 'string' - }; - - const castedType = typeCastings[type]; - if (!castedType) { - throw new Error(`Unsupported script sort type: ${type}`); - } - - return castedType; -} diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts similarity index 88% rename from src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js rename to src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts index b220361e33b3b..522117fe22804 100644 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts @@ -23,11 +23,8 @@ test('Should exclude docvalue_fields that are not contained in fields', () => { const docvalueFields = [ 'my_ip_field', { field: 'my_keyword_field' }, - { field: 'my_date_field', 'format': 'epoch_millis' } + { field: 'my_date_field', format: 'epoch_millis' }, ]; const out = filterDocvalueFields(docvalueFields, ['my_ip_field', 'my_keyword_field']); - expect(out).toEqual([ - 'my_ip_field', - { field: 'my_keyword_field' }, - ]); + expect(out).toEqual(['my_ip_field', { field: 'my_keyword_field' }]); }); diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts similarity index 84% rename from src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js rename to src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts index cd726709b4b5c..917d26f0decd1 100644 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts @@ -17,7 +17,15 @@ * under the License. */ -export function filterDocvalueFields(docvalueFields, fields) { +interface DocvalueField { + field: string; + [key: string]: unknown; +} + +export function filterDocvalueFields( + docvalueFields: Array, + fields: string[] +) { return docvalueFields.filter(docValue => { const docvalueFieldName = typeof docValue === 'string' ? docValue : docValue.field; return fields.includes(docvalueFieldName); diff --git a/src/legacy/ui/public/courier/search_source/index.d.ts b/src/legacy/ui/public/courier/search_source/index.d.ts deleted file mode 100644 index dcae7b3d2ff05..0000000000000 --- a/src/legacy/ui/public/courier/search_source/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { SearchSource } from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/index.js b/src/legacy/ui/public/courier/search_source/index.ts similarity index 94% rename from src/legacy/ui/public/courier/search_source/index.js rename to src/legacy/ui/public/courier/search_source/index.ts index dcae7b3d2ff05..72170adc2b129 100644 --- a/src/legacy/ui/public/courier/search_source/index.js +++ b/src/legacy/ui/public/courier/search_source/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SearchSource } from './search_source'; +export * from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/mocks.ts b/src/legacy/ui/public/courier/search_source/mocks.ts index bf546c1b9e7c2..2b83f379b4f09 100644 --- a/src/legacy/ui/public/courier/search_source/mocks.ts +++ b/src/legacy/ui/public/courier/search_source/mocks.ts @@ -36,21 +36,22 @@ * under the License. */ -export const searchSourceMock = { +import { SearchSourceContract } from './search_source'; + +export const searchSourceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), - getPreferredSearchStrategyId: jest.fn(), - setFields: jest.fn(), - setField: jest.fn(), + setFields: jest.fn().mockReturnThis(), + setField: jest.fn().mockReturnThis(), getId: jest.fn(), getFields: jest.fn(), getField: jest.fn(), getOwnField: jest.fn(), - create: jest.fn(), - createCopy: jest.fn(), - createChild: jest.fn(), + create: jest.fn().mockReturnThis(), + createCopy: jest.fn().mockReturnThis(), + createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), - getParent: jest.fn(), - fetch: jest.fn(), + getParent: jest.fn().mockReturnThis(), + fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), destroy: jest.fn(), diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts new file mode 100644 index 0000000000000..d27b01eb5cf7c --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { normalizeSortRequest } from './normalize_sort_request'; +import { SortDirection } from './types'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +jest.mock('ui/new_platform'); + +describe('SearchSource#normalizeSortRequest', function() { + const scriptedField = { + name: 'script string', + type: 'number', + scripted: true, + sortable: true, + script: 'foo', + lang: 'painless', + }; + const murmurScriptedField = { + ...scriptedField, + sortable: false, + name: 'murmur script', + type: 'murmur3', + }; + const indexPattern = { + fields: [scriptedField, murmurScriptedField], + } as IndexPattern; + + it('should return an array', function() { + const sortable = { someField: SortDirection.desc }; + const result = normalizeSortRequest(sortable, indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + // ensure object passed in is not mutated + expect(result[0]).not.toBe(sortable); + expect(sortable).toEqual({ someField: SortDirection.desc }); + }); + + it('should make plain string sort into the more verbose format', function() { + const result = normalizeSortRequest([{ someField: SortDirection.desc }], indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should append default sort options', function() { + const defaultSortOptions = { + unmapped_type: 'boolean', + }; + const result = normalizeSortRequest( + [{ someField: SortDirection.desc }], + indexPattern, + defaultSortOptions + ); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + ...defaultSortOptions, + }, + }, + ]); + }); + + it('should enable script based sorting', function() { + const result = normalizeSortRequest( + { + [scriptedField.name]: SortDirection.desc, + }, + indexPattern + ); + expect(result).toEqual([ + { + _script: { + script: { + source: scriptedField.script, + lang: scriptedField.lang, + }, + type: scriptedField.type, + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should use script based sorting only on sortable types', function() { + const result = normalizeSortRequest( + [ + { + [murmurScriptedField.name]: SortDirection.asc, + }, + ], + indexPattern + ); + + expect(result).toEqual([ + { + [murmurScriptedField.name]: { + order: SortDirection.asc, + }, + }, + ]); + }); + + it('should remove unmapped_type parameter from _score sorting', function() { + const result = normalizeSortRequest({ _score: SortDirection.desc }, indexPattern, { + unmapped_type: 'boolean', + }); + expect(result).toEqual([ + { + _score: { + order: SortDirection.desc, + }, + }, + ]); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts new file mode 100644 index 0000000000000..0f8fc8076caa0 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts @@ -0,0 +1,78 @@ +/* + * 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 '../../../../core_plugins/data/public'; +import { EsQuerySortValue, SortOptions } from './types'; + +export function normalizeSortRequest( + sortObject: EsQuerySortValue | EsQuerySortValue[], + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: SortOptions = {} +) { + const sortArray: EsQuerySortValue[] = Array.isArray(sortObject) ? sortObject : [sortObject]; + return sortArray.map(function(sortable) { + return normalize(sortable, indexPattern, defaultSortOptions); + }); +} + +/** + * Normalize the sort description to the more verbose format (e.g. { someField: "desc" } into + * { someField: { "order": "desc"}}), and convert sorts on scripted fields into the proper script + * for Elasticsearch. Mix in the default options according to the advanced settings. + */ +function normalize( + sortable: EsQuerySortValue, + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: any +) { + const [[sortField, sortOrder]] = Object.entries(sortable); + const order = typeof sortOrder === 'object' ? sortOrder : { order: sortOrder }; + + if (indexPattern && typeof indexPattern !== 'string') { + const indexField = indexPattern.fields.find(({ name }) => name === sortField); + if (indexField && indexField.scripted && indexField.sortable) { + return { + _script: { + script: { + source: indexField.script, + lang: indexField.lang, + }, + type: castSortType(indexField.type), + ...order, + }, + }; + } + } + + // Don't include unmapped_type for _score field + const { unmapped_type, ...otherSortOptions } = defaultSortOptions; + return { + [sortField]: { ...order, ...(sortField === '_score' ? otherSortOptions : defaultSortOptions) }, + }; +} + +// The ES API only supports sort scripts of type 'number' and 'string' +function castSortType(type: string) { + if (['number', 'string'].includes(type)) { + return 'number'; + } else if (['string', 'boolean'].includes(type)) { + return 'string'; + } + throw new Error(`Unsupported script sort type: ${type}`); +} diff --git a/src/legacy/ui/public/courier/search_source/search_source.d.ts b/src/legacy/ui/public/courier/search_source/search_source.d.ts deleted file mode 100644 index 674e7ace0594c..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export declare class SearchSource { - setPreferredSearchStrategyId: (searchStrategyId: string) => void; - getPreferredSearchStrategyId: () => string; - setFields: (newFields: any) => SearchSource; - setField: (field: string, value: any) => SearchSource; - getId: () => string; - getFields: () => any; - getField: (field: string) => any; - getOwnField: () => any; - create: () => SearchSource; - createCopy: () => SearchSource; - createChild: (options?: any) => SearchSource; - setParent: (parent: SearchSource | boolean) => SearchSource; - getParent: () => SearchSource | undefined; - fetch: (options?: any) => Promise; - onRequestStart: (handler: (searchSource: SearchSource, options: any) => void) => void; - getSearchRequestBody: () => any; - destroy: () => void; - history: any[]; -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.js b/src/legacy/ui/public/courier/search_source/search_source.js deleted file mode 100644 index bc69e862fea48..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.js +++ /dev/null @@ -1,540 +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. - */ - -/** - * @name SearchSource - * - * @description A promise-based stream of search results that can inherit from other search sources. - * - * Because filters/queries in Kibana have different levels of persistence and come from different - * places, it is important to keep track of where filters come from for when they are saved back to - * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects - * that can have associated query parameters (index, query, filter, etc) which can also inherit from - * other searchSource objects. - * - * At query time, all of the searchSource objects that have subscribers are "flattened", at which - * point the query params from the searchSource are collected while traversing up the inheritance - * chain. At each link in the chain a decision about how to merge the query params is made until a - * single set of query parameters is created for each active searchSource (a searchSource with - * subscribers). - * - * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy - * works in Kibana. - * - * Visualize, starting from a new search: - * - * - the `savedVis.searchSource` is set as the `appSearchSource`. - * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is - * upgraded to inherit from the `rootSearchSource`. - * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so - * they will be stored directly on the `savedVis.searchSource`. - * - Any interaction with the time filter will be written to the `rootSearchSource`, so those - * filters will not be saved by the `savedVis`. - * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are - * defined on it directly, but none of the ones that it inherits from other places. - * - * Visualize, starting from an existing search: - * - * - The `savedVis` loads the `savedSearch` on which it is built. - * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as - * the `appSearchSource`. - * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. - * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the - * filters created in the visualize application and will reconnect the filters from the - * `savedSearch` at runtime to prevent losing the relationship - * - * Dashboard search sources: - * - * - Each panel in a dashboard has a search source. - * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. - * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from - * the dashboard search source. - * - When a filter is added to the search box, or via a visualization, it is written to the - * `appSearchSource`. - */ - -import _ from 'lodash'; -import angular from 'angular'; - -import { normalizeSortRequest } from './_normalize_sort_request'; - -import { fetchSoon } from '../fetch'; -import { fieldWildcardFilter } from '../../field_wildcard'; -import { getHighlightRequest, esQuery } from '../../../../../plugins/data/public'; -import { npSetup } from 'ui/new_platform'; -import chrome from '../../chrome'; -import { RequestFailure } from '../fetch/errors'; -import { filterDocvalueFields } from './filter_docvalue_fields'; - -const FIELDS = [ - 'type', - 'query', - 'filter', - 'sort', - 'highlight', - 'highlightAll', - 'aggs', - 'from', - 'searchAfter', - 'size', - 'source', - 'version', - 'fields', - 'index', -]; - -function parseInitialFields(initialFields) { - if (!initialFields) { - return {}; - } - - return typeof initialFields === 'string' ? - JSON.parse(initialFields) - : _.cloneDeep(initialFields); -} - -function isIndexPattern(val) { - return Boolean(val && typeof val.title === 'string'); -} - -const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout'); -const config = npSetup.core.uiSettings; -const getConfig = (...args) => config.get(...args); -const forIp = Symbol('for which index pattern?'); - -export class SearchSource { - constructor(initialFields) { - this._id = _.uniqueId('data_source'); - - this._searchStrategyId = undefined; - this._fields = parseInitialFields(initialFields); - this._parent = undefined; - - this.history = []; - this._requestStartHandlers = []; - this._inheritOptions = {}; - } - - /***** - * PUBLIC API - *****/ - - setPreferredSearchStrategyId(searchStrategyId) { - this._searchStrategyId = searchStrategyId; - } - - getPreferredSearchStrategyId() { - return this._searchStrategyId; - } - - setFields(newFields) { - this._fields = newFields; - return this; - } - - setField(field, value) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't set field '${field}' on SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - if (field === 'index') { - const fields = this._fields; - - const hasSource = fields.source; - const sourceCameFromIp = hasSource && fields.source.hasOwnProperty(forIp); - const sourceIsForOurIp = sourceCameFromIp && fields.source[forIp] === fields.index; - if (sourceIsForOurIp) { - delete fields.source; - } - - if (value === null || value === undefined) { - delete fields.index; - return this; - } - - if (!isIndexPattern(value)) { - throw new TypeError('expected indexPattern to be an IndexPattern duck.'); - } - - fields[field] = value; - if (!fields.source) { - // imply source filtering based on the index pattern, but allow overriding - // it by simply setting another field for "source". When index is changed - fields.source = function () { - return value.getSourceFiltering(); - }; - fields.source[forIp] = value; - } - - return this; - } - - if (value == null) { - delete this._fields[field]; - return this; - } - - this._fields[field] = value; - return this; - } - - getId() { - return this._id; - } - - getFields() { - return _.clone(this._fields); - } - - /** - * Get fields from the fields - */ - getField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - let searchSource = this; - - while (searchSource) { - const value = searchSource._fields[field]; - if (value !== void 0) { - return value; - } - - searchSource = searchSource.getParent(); - } - } - - /** - * Get the field from our own fields, don't traverse up the chain - */ - getOwnField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - const value = this._fields[field]; - if (value !== void 0) { - return value; - } - } - - create() { - return new SearchSource(); - } - - createCopy() { - const json = angular.toJson(this._fields); - const newSearchSource = new SearchSource(json); - // when serializing the internal fields we lose the internal classes used in the index - // pattern, so we have to set it again to workaround this behavior - newSearchSource.setField('index', this.getField('index')); - newSearchSource.setParent(this.getParent()); - return newSearchSource; - } - - createChild(options = {}) { - const childSearchSource = new SearchSource(); - childSearchSource.setParent(this, options); - return childSearchSource; - } - - /** - * Set a searchSource that this source should inherit from - * @param {SearchSource} searchSource - the parent searchSource - * @return {this} - chainable - */ - setParent(parent, options = {}) { - this._parent = parent; - this._inheritOptions = options; - return this; - } - - /** - * Get the parent of this SearchSource - * @return {undefined|searchSource} - */ - getParent() { - return this._parent || undefined; - } - - /** - * Fetch this source and reject the returned Promise on error - * - * @async - */ - async fetch(options) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const es = $injector.get('es'); - - await this.requestIsStarting(options); - - const searchRequest = await this._flatten(); - this.history = [searchRequest]; - - const response = await fetchSoon(searchRequest, { - ...(this._searchStrategyId && { searchStrategyId: this._searchStrategyId }), - ...options, - }, { es, config, esShardTimeout }); - - if (response.error) { - throw new RequestFailure(null, response); - } - - return response; - } - - /** - * Add a handler that will be notified whenever requests start - * @param {Function} handler - * @return {undefined} - */ - onRequestStart(handler) { - this._requestStartHandlers.push(handler); - } - - /** - * Called by requests of this search source when they are started - * @param {Courier.Request} request - * @param options - * @return {Promise} - */ - requestIsStarting(options) { - const handlers = [...this._requestStartHandlers]; - // If callparentStartHandlers has been set to true, we also call all - // handlers of parent search sources. - if (this._inheritOptions.callParentStartHandlers) { - let searchSource = this.getParent(); - while (searchSource) { - handlers.push(...searchSource._requestStartHandlers); - searchSource = searchSource.getParent(); - } - } - - return Promise.all(handlers.map(fn => fn(this, options))); - } - - async getSearchRequestBody() { - const searchRequest = await this._flatten(); - return searchRequest.body; - } - - /** - * Completely destroy the SearchSource. - * @return {undefined} - */ - destroy() { - this._requestStartHandlers.length = 0; - } - - /****** - * PRIVATE APIS - ******/ - - /** - * Used to merge properties into the data within ._flatten(). - * The data is passed in and modified by the function - * - * @param {object} data - the current merged data - * @param {*} val - the value at `key` - * @param {*} key - The key of `val` - * @return {undefined} - */ - _mergeProp(data, val, key) { - if (typeof val === 'function') { - const source = this; - return Promise.resolve(val(this)) - .then(function (newVal) { - return source._mergeProp(data, newVal, key); - }); - } - - if (val == null || !key || !_.isString(key)) return; - - switch (key) { - case 'filter': - const filters = Array.isArray(val) ? val : [val]; - data.filters = [...(data.filters || []), ...filters]; - return; - case 'index': - case 'type': - case 'id': - case 'highlightAll': - if (key && data[key] == null) { - data[key] = val; - } - return; - case 'searchAfter': - key = 'search_after'; - addToBody(); - break; - case 'source': - key = '_source'; - addToBody(); - break; - case 'sort': - val = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); - addToBody(); - break; - case 'query': - data.query = (data.query || []).concat(val); - break; - case 'fields': - data[key] = _.uniq([...(data[key] || []), ...val]); - break; - default: - addToBody(); - } - - /** - * Add the key and val to the body of the request - */ - function addToBody() { - data.body = data.body || {}; - // ignore if we already have a value - if (data.body[key] == null) { - data.body[key] = val; - } - } - } - - /** - * Walk the inheritance chain of a source and return it's - * flat representation (taking into account merging rules) - * @returns {Promise} - * @resolved {Object|null} - the flat data of the SearchSource - */ - _flatten() { - // the merged data of this dataSource and it's ancestors - const flatData = {}; - - // function used to write each property from each data object in the chain to flat data - const root = this; - - // start the chain at this source - let current = this; - - // call the ittr and return it's promise - return (function ittr() { - // iterate the _fields object (not array) and - // pass each key:value pair to source._mergeProp. if _mergeProp - // returns a promise, then wait for it to complete and call _mergeProp again - return Promise.all(_.map(current._fields, function ittr(value, key) { - if (value instanceof Promise) { - return value.then(function (value) { - return ittr(value, key); - }); - } - - const prom = root._mergeProp(flatData, value, key); - return prom instanceof Promise ? prom : null; - })) - .then(function () { - // move to this sources parent - const parent = current.getParent(); - // keep calling until we reach the top parent - if (parent) { - current = parent; - return ittr(); - } - }); - }()) - .then(function () { - // This is down here to prevent the circular dependency - flatData.body = flatData.body || {}; - - const computedFields = flatData.index.getComputedFields(); - - flatData.body.stored_fields = computedFields.storedFields; - flatData.body.script_fields = flatData.body.script_fields || {}; - _.extend(flatData.body.script_fields, computedFields.scriptFields); - - const defaultDocValueFields = computedFields.docvalueFields ? computedFields.docvalueFields : []; - flatData.body.docvalue_fields = flatData.body.docvalue_fields || defaultDocValueFields; - - if (flatData.body._source) { - // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(flatData.body._source.excludes, config.get('metaFields')); - flatData.body.docvalue_fields = flatData.body.docvalue_fields.filter( - docvalueField => filter(docvalueField.field) - ); - } - - // if we only want to search for certain fields - const fields = flatData.fields; - if (fields) { - // filter out the docvalue_fields, and script_fields to only include those that we are concerned with - flatData.body.docvalue_fields = filterDocvalueFields(flatData.body.docvalue_fields, fields); - flatData.body.script_fields = _.pick(flatData.body.script_fields, fields); - - // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(flatData.body.script_fields)); - flatData.body.stored_fields = remainingFields; - _.set(flatData.body, '_source.includes', remainingFields); - } - - const esQueryConfigs = esQuery.getEsQueryConfig(config); - flatData.body.query = esQuery.buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs); - - if (flatData.highlightAll != null) { - if (flatData.highlightAll && flatData.body.query) { - flatData.body.highlight = getHighlightRequest(flatData.body.query, getConfig); - } - delete flatData.highlightAll; - } - - /** - * Translate a filter into a query to support es 3+ - * @param {Object} filter - The filter to translate - * @return {Object} the query version of that filter - */ - const translateToQuery = function (filter) { - if (!filter) return; - - if (filter.query) { - return filter.query; - } - - return filter; - }; - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function (id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const filters = agg.filters.filters; - - Object.keys(filters).forEach(function (filterId) { - filters[filterId] = translateToQuery(filters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - }(flatData.body.aggs || flatData.body.aggregations)); - - return flatData; - }); - } -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.js b/src/legacy/ui/public/courier/search_source/search_source.test.js deleted file mode 100644 index 800f4e4308671..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.test.js +++ /dev/null @@ -1,193 +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 { SearchSource } from '../search_source'; - -jest.mock('ui/new_platform', () => ({ - npSetup: { - core: { - injectedMetadata: { - getInjectedVar: () => 0, - } - } - } -})); - -jest.mock('../fetch', () => ({ - fetchSoon: jest.fn(), -})); - -const indexPattern = { title: 'foo' }; -const indexPattern2 = { title: 'foo' }; - -describe('SearchSource', function () { - describe('#setField()', function () { - it('sets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.setField('index', 5)).toThrow(); - }); - }); - - describe('#getField()', function () { - it('gets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.getField('unacceptablePropName')).toThrow(); - }); - }); - - describe(`#setField('index')`, function () { - describe('auto-sourceFiltering', function () { - describe('new index pattern assigned', function () { - it('generates a searchSource filter', function () { - const searchSource = new SearchSource(); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('new index pattern assigned over another', function () { - it('replaces searchSource filter with new', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - const searchSourceFilter1 = searchSource.getField('source'); - searchSource.setField('index', indexPattern2); - expect(searchSource.getField('index')).toBe(indexPattern2); - expect(typeof searchSource.getField('source')).toBe('function'); - expect(searchSource.getField('source')).not.toBe(searchSourceFilter1); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', indexPattern2); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('ip assigned before custom searchSource filter', function () { - it('custom searchSource filter becomes new searchSource', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - searchSource.setField('source', football); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - searchSource.setField('source', football); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - - describe('ip assigned after custom searchSource filter', function () { - it('leaves the custom filter in place', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - }); - }); - - describe('#onRequestStart()', () => { - it('should be called when starting a request', () => { - const searchSource = new SearchSource(); - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const options = {}; - searchSource.requestIsStarting(options); - expect(fn).toBeCalledWith(searchSource, options); - }); - - it('should not be called on parent searchSource', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).not.toBeCalled(); - }); - - it('should be called on parent searchSource if callParentStartHandlers is true', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent, { callParentStartHandlers: true }); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).toBeCalledWith(searchSource, options); - }); - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.ts b/src/legacy/ui/public/courier/search_source/search_source.test.ts new file mode 100644 index 0000000000000..ddd3717f55e29 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { SearchSource } from '../search_source'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +jest.mock('ui/new_platform'); + +jest.mock('../fetch', () => ({ + fetchSoon: jest.fn().mockResolvedValue({}), +})); + +jest.mock('../../chrome', () => ({ + dangerouslyGetActiveInjector: () => ({ + get: jest.fn(), + }), +})); + +const getComputedFields = () => ({ + storedFields: [], + scriptFields: [], + docvalueFields: [], +}); +const mockSource = { excludes: ['foo-*'] }; +const mockSource2 = { excludes: ['bar-*'] }; +const indexPattern = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource, +} as unknown) as IndexPattern; +const indexPattern2 = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource2, +} as unknown) as IndexPattern; + +describe('SearchSource', function() { + describe('#setField()', function() { + it('sets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe('#getField()', function() { + it('gets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe(`#setField('index')`, function() { + describe('auto-sourceFiltering', function() { + describe('new index pattern assigned', function() { + it('generates a searchSource filter', async function() { + const searchSource = new SearchSource(); + expect(searchSource.getField('index')).toBe(undefined); + expect(searchSource.getField('source')).toBe(undefined); + searchSource.setField('index', indexPattern); + expect(searchSource.getField('index')).toBe(indexPattern); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + + describe('new index pattern assigned over another', function() { + it('replaces searchSource filter with new', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + expect(searchSource.getField('index')).toBe(indexPattern2); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource2); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + }); + }); + + describe('#onRequestStart()', () => { + it('should be called when starting a request', async () => { + const searchSource = new SearchSource({ index: indexPattern }); + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const options = {}; + await searchSource.fetch(options); + expect(fn).toBeCalledWith(searchSource, options); + }); + + it('should not be called on parent searchSource', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).not.toBeCalled(); + }); + + it('should be called on parent searchSource if callParentStartHandlers is true', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }).setParent(parent, { + callParentStartHandlers: true, + }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).toBeCalledWith(searchSource, options); + }); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.ts b/src/legacy/ui/public/courier/search_source/search_source.ts new file mode 100644 index 0000000000000..e862bb1118a74 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.ts @@ -0,0 +1,410 @@ +/* + * 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. + */ + +/** + * @name SearchSource + * + * @description A promise-based stream of search results that can inherit from other search sources. + * + * Because filters/queries in Kibana have different levels of persistence and come from different + * places, it is important to keep track of where filters come from for when they are saved back to + * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects + * that can have associated query parameters (index, query, filter, etc) which can also inherit from + * other searchSource objects. + * + * At query time, all of the searchSource objects that have subscribers are "flattened", at which + * point the query params from the searchSource are collected while traversing up the inheritance + * chain. At each link in the chain a decision about how to merge the query params is made until a + * single set of query parameters is created for each active searchSource (a searchSource with + * subscribers). + * + * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy + * works in Kibana. + * + * Visualize, starting from a new search: + * + * - the `savedVis.searchSource` is set as the `appSearchSource`. + * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is + * upgraded to inherit from the `rootSearchSource`. + * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so + * they will be stored directly on the `savedVis.searchSource`. + * - Any interaction with the time filter will be written to the `rootSearchSource`, so those + * filters will not be saved by the `savedVis`. + * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are + * defined on it directly, but none of the ones that it inherits from other places. + * + * Visualize, starting from an existing search: + * + * - The `savedVis` loads the `savedSearch` on which it is built. + * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as + * the `appSearchSource`. + * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. + * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the + * filters created in the visualize application and will reconnect the filters from the + * `savedSearch` at runtime to prevent losing the relationship + * + * Dashboard search sources: + * + * - Each panel in a dashboard has a search source. + * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. + * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from + * the dashboard search source. + * - When a filter is added to the search box, or via a visualization, it is written to the + * `appSearchSource`. + */ + +import _ from 'lodash'; +import { npSetup } from 'ui/new_platform'; +import { normalizeSortRequest } from './normalize_sort_request'; +import { fetchSoon } from '../fetch'; +import { fieldWildcardFilter } from '../../field_wildcard'; +import { getHighlightRequest, esFilters, esQuery } from '../../../../../plugins/data/public'; +import chrome from '../../chrome'; +import { RequestFailure } from '../fetch/errors'; +import { filterDocvalueFields } from './filter_docvalue_fields'; +import { SearchSourceOptions, SearchSourceFields, SearchRequest } from './types'; +import { FetchOptions, ApiCaller } from '../fetch/types'; + +const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout') as number; +const config = npSetup.core.uiSettings; + +export type SearchSourceContract = Pick; + +export class SearchSource { + private id: string = _.uniqueId('data_source'); + private searchStrategyId?: string; + private parent?: SearchSource; + private requestStartHandlers: Array< + (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + > = []; + private inheritOptions: SearchSourceOptions = {}; + public history: SearchRequest[] = []; + + constructor(private fields: SearchSourceFields = {}) {} + + /** *** + * PUBLIC API + *****/ + + setPreferredSearchStrategyId(searchStrategyId: string) { + this.searchStrategyId = searchStrategyId; + } + + setFields(newFields: SearchSourceFields) { + this.fields = newFields; + return this; + } + + setField(field: K, value: SearchSourceFields[K]) { + if (value == null) { + delete this.fields[field]; + } else { + this.fields[field] = value; + } + return this; + } + + getId() { + return this.id; + } + + getFields() { + return { ...this.fields }; + } + + /** + * Get fields from the fields + */ + getField(field: K, recurse = true): SearchSourceFields[K] { + if (!recurse || this.fields[field] !== void 0) { + return this.fields[field]; + } + const parent = this.getParent(); + return parent && parent.getField(field); + } + + /** + * Get the field from our own fields, don't traverse up the chain + */ + getOwnField(field: K): SearchSourceFields[K] { + return this.getField(field, false); + } + + create() { + return new SearchSource(); + } + + createCopy() { + const newSearchSource = new SearchSource(); + newSearchSource.setFields({ ...this.fields }); + // when serializing the internal fields we lose the internal classes used in the index + // pattern, so we have to set it again to workaround this behavior + newSearchSource.setField('index', this.getField('index')); + newSearchSource.setParent(this.getParent()); + return newSearchSource; + } + + createChild(options = {}) { + const childSearchSource = new SearchSource(); + childSearchSource.setParent(this, options); + return childSearchSource; + } + + /** + * Set a searchSource that this source should inherit from + * @param {SearchSource} parent - the parent searchSource + * @param {SearchSourceOptions} options - the inherit options + * @return {this} - chainable + */ + setParent(parent?: SearchSourceContract, options: SearchSourceOptions = {}) { + this.parent = parent as SearchSource; + this.inheritOptions = options; + return this; + } + + /** + * Get the parent of this SearchSource + * @return {undefined|searchSource} + */ + getParent() { + return this.parent; + } + + /** + * Fetch this source and reject the returned Promise on error + * + * @async + */ + async fetch(options: FetchOptions = {}) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const es = $injector.get('es') as ApiCaller; + + await this.requestIsStarting(options); + + const searchRequest = await this.flatten(); + this.history = [searchRequest]; + + const response = await fetchSoon( + searchRequest, + { + ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), + ...options, + }, + { es, config, esShardTimeout } + ); + + if (response.error) { + throw new RequestFailure(null, response); + } + + return response; + } + + /** + * Add a handler that will be notified whenever requests start + * @param {Function} handler + * @return {undefined} + */ + onRequestStart( + handler: (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + ) { + this.requestStartHandlers.push(handler); + } + + async getSearchRequestBody() { + const searchRequest = await this.flatten(); + return searchRequest.body; + } + + /** + * Completely destroy the SearchSource. + * @return {undefined} + */ + destroy() { + this.requestStartHandlers.length = 0; + } + + /** **** + * PRIVATE APIS + ******/ + + /** + * Called by requests of this search source when they are started + * @param {Courier.Request} request + * @param options + * @return {Promise} + */ + private requestIsStarting(options: FetchOptions = {}) { + const handlers = [...this.requestStartHandlers]; + // If callParentStartHandlers has been set to true, we also call all + // handlers of parent search sources. + if (this.inheritOptions.callParentStartHandlers) { + let searchSource = this.getParent(); + while (searchSource) { + handlers.push(...searchSource.requestStartHandlers); + searchSource = searchSource.getParent(); + } + } + + return Promise.all(handlers.map(fn => fn(this, options))); + } + + /** + * Used to merge properties into the data within ._flatten(). + * The data is passed in and modified by the function + * + * @param {object} data - the current merged data + * @param {*} val - the value at `key` + * @param {*} key - The key of `val` + * @return {undefined} + */ + private mergeProp( + data: SearchRequest, + val: SearchSourceFields[K], + key: K + ) { + val = typeof val === 'function' ? val(this) : val; + if (val == null || !key) return; + + const addToRoot = (rootKey: string, value: any) => { + data[rootKey] = value; + }; + + /** + * Add the key and val to the body of the request + */ + const addToBody = (bodyKey: string, value: any) => { + // ignore if we already have a value + if (data.body[bodyKey] == null) { + data.body[bodyKey] = value; + } + }; + + switch (key) { + case 'filter': + return addToRoot('filters', (data.filters || []).concat(val)); + case 'query': + return addToRoot(key, (data[key] || []).concat(val)); + case 'fields': + const fields = _.uniq((data[key] || []).concat(val)); + return addToRoot(key, fields); + case 'index': + case 'type': + case 'highlightAll': + return key && data[key] == null && addToRoot(key, val); + case 'searchAfter': + return addToBody('search_after', val); + case 'source': + return addToBody('_source', val); + case 'sort': + const sort = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); + return addToBody(key, sort); + default: + return addToBody(key, val); + } + } + + /** + * Walk the inheritance chain of a source and return its + * flat representation (taking into account merging rules) + * @returns {Promise} + * @resolved {Object|null} - the flat data of the SearchSource + */ + private mergeProps(root = this, searchRequest: SearchRequest = { body: {} }) { + Object.entries(this.fields).forEach(([key, value]) => { + this.mergeProp(searchRequest, value, key as keyof SearchSourceFields); + }); + if (this.parent) { + this.parent.mergeProps(root, searchRequest); + } + return searchRequest; + } + + private flatten() { + const searchRequest = this.mergeProps(); + + searchRequest.body = searchRequest.body || {}; + const { body, index, fields, query, filters, highlightAll } = searchRequest; + + const computedFields = index ? index.getComputedFields() : {}; + + body.stored_fields = computedFields.storedFields; + body.script_fields = body.script_fields || {}; + _.extend(body.script_fields, computedFields.scriptFields); + + const defaultDocValueFields = computedFields.docvalueFields + ? computedFields.docvalueFields + : []; + body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; + + if (!body.hasOwnProperty('_source') && index) { + body._source = index.getSourceFiltering(); + } + + if (body._source) { + // exclude source fields for this index pattern specified by the user + const filter = fieldWildcardFilter(body._source.excludes, config.get('metaFields')); + body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => + filter(docvalueField.field) + ); + } + + // if we only want to search for certain fields + if (fields) { + // filter out the docvalue_fields, and script_fields to only include those that we are concerned with + body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); + body.script_fields = _.pick(body.script_fields, fields); + + // request the remaining fields from both stored_fields and _source + const remainingFields = _.difference(fields, _.keys(body.script_fields)); + body.stored_fields = remainingFields; + _.set(body, '_source.includes', remainingFields); + } + + const esQueryConfigs = esQuery.getEsQueryConfig(config); + body.query = esQuery.buildEsQuery(index, query, filters, esQueryConfigs); + + if (highlightAll && body.query) { + body.highlight = getHighlightRequest(body.query, config.get('doc_table:highlight')); + delete searchRequest.highlightAll; + } + + const translateToQuery = (filter: esFilters.Filter) => filter && (filter.query || filter); + + // re-write filters within filter aggregations + (function recurse(aggBranch) { + if (!aggBranch) return; + Object.keys(aggBranch).forEach(function(id) { + const agg = aggBranch[id]; + + if (agg.filters) { + // translate filters aggregations + const { filters: aggFilters } = agg.filters; + Object.keys(aggFilters).forEach(filterId => { + aggFilters[filterId] = translateToQuery(aggFilters[filterId]); + }); + } + + recurse(agg.aggs || agg.aggregations); + }); + })(body.aggs || body.aggregations); + + return searchRequest; + } +} diff --git a/src/legacy/ui/public/courier/search_source/types.ts b/src/legacy/ui/public/courier/search_source/types.ts new file mode 100644 index 0000000000000..293f3d49596c3 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/types.ts @@ -0,0 +1,106 @@ +/* + * 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 { NameList } from 'elasticsearch'; +import { esFilters, Query } from '../../../../../plugins/data/public'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +export type EsQuerySearchAfter = [string | number, string | number]; + +export enum SortDirection { + asc = 'asc', + desc = 'desc', +} + +export type EsQuerySortValue = Record; + +export interface SearchSourceFields { + type?: string; + query?: Query; + filter?: + | esFilters.Filter[] + | esFilters.Filter + | (() => esFilters.Filter[] | esFilters.Filter | undefined); + sort?: EsQuerySortValue | EsQuerySortValue[]; + highlight?: any; + highlightAll?: boolean; + aggs?: any; + from?: number; + size?: number; + source?: NameList; + version?: boolean; + fields?: NameList; + index?: IndexPattern; + searchAfter?: EsQuerySearchAfter; +} + +export interface SearchSourceOptions { + callParentStartHandlers?: boolean; +} + +export { SearchSourceContract } from './search_source'; + +export interface SortOptions { + mode?: 'min' | 'max' | 'sum' | 'avg' | 'median'; + type?: 'double' | 'long' | 'date' | 'date_nanos'; + nested?: object; + unmapped_type?: string; + distance_type?: 'arc' | 'plane'; + unit?: string; + ignore_unmapped?: boolean; + _script?: object; +} + +export interface Request { + docvalue_fields: string[]; + _source: unknown; + query: unknown; + script_fields: unknown; + sort: unknown; + stored_fields: string[]; +} + +export interface ResponseWithShardFailure { + _shards: { + failed: number; + failures: ShardFailure[]; + skipped: number; + successful: number; + total: number; + }; +} + +export interface ShardFailure { + index: string; + node: string; + reason: { + caused_by: { + reason: string; + type: string; + }; + reason: string; + lang?: string; + script?: string; + script_stack?: string[]; + type: string; + }; + shard: number; +} + +export type SearchRequest = any; +export type SearchResponse = any; diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts similarity index 67% rename from src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js rename to src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts index a1ea53e8b5b47..29921fc7a11d3 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts @@ -18,26 +18,28 @@ */ import { defaultSearchStrategy } from './default_search_strategy'; +import { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchStrategySearchParams } from './types'; const { search } = defaultSearchStrategy; -function getConfigStub(config = {}) { +function getConfigStub(config: any = {}) { return { - get: key => config[key] - }; + get: key => config[key], + } as UiSettingsClientContract; } -const msearchMockResponse = Promise.resolve([]); +const msearchMockResponse: any = Promise.resolve([]); msearchMockResponse.abort = jest.fn(); const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); -const searchMockResponse = Promise.resolve([]); +const searchMockResponse: any = Promise.resolve([]); searchMockResponse.abort = jest.fn(); const searchMock = jest.fn().mockReturnValue(searchMockResponse); -describe('defaultSearchStrategy', function () { - describe('search', function () { - let searchArgs; +describe('defaultSearchStrategy', function() { + describe('search', function() { + let searchArgs: MockedKeys>; beforeEach(() => { msearchMockResponse.abort.mockClear(); @@ -47,9 +49,12 @@ describe('defaultSearchStrategy', function () { searchMock.mockClear(); searchArgs = { - searchRequests: [{ - index: { title: 'foo' } - }], + searchRequests: [ + { + index: { title: 'foo' }, + }, + ], + esShardTimeout: 0, es: { msearch: msearchMock, search: searchMock, @@ -58,48 +63,48 @@ describe('defaultSearchStrategy', function () { }); test('does not send max_concurrent_shard_requests by default', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { - searchArgs.config = getConfigStub({ + const config = getConfigStub({ 'courier:batchSearches': true, 'courier:maxConcurrentShardRequests': 42, }); - await search(searchArgs); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { - searchArgs.config = getConfigStub({ + const config = getConfigStub({ 'courier:batchSearches': true, 'search:includeFrozen': true, }); - await search(searchArgs); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); test('should properly call abort with msearch', () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); - search(searchArgs).abort(); + search({ ...searchArgs, config }).abort(); expect(msearchMockResponse.abort).toHaveBeenCalled(); }); test('should properly abort with search', async () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': false + const config = getConfigStub({ + 'courier:batchSearches': false, }); - search(searchArgs).abort(); + search({ ...searchArgs, config }).abort(); expect(searchMockResponse.abort).toHaveBeenCalled(); }); }); diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts similarity index 76% rename from src/legacy/ui/public/courier/search_strategy/default_search_strategy.js rename to src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts index 42a9b64136454..5be4fef076655 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts @@ -17,37 +17,39 @@ * under the License. */ +import { SearchStrategyProvider, SearchStrategySearchParams } from './types'; import { addSearchStrategy } from './search_strategy_registry'; import { isDefaultTypeIndexPattern } from './is_default_type_index_pattern'; -import { getSearchParams, getMSearchParams, getPreference, getTimeout } from '../fetch/get_search_params'; +import { + getSearchParams, + getMSearchParams, + getPreference, + getTimeout, +} from '../fetch/get_search_params'; -export const defaultSearchStrategy = { +export const defaultSearchStrategy: SearchStrategyProvider = { id: 'default', search: params => { return params.config.get('courier:batchSearches') ? msearch(params) : search(params); }, - isViable: (indexPattern) => { - if (!indexPattern) { - return false; - } - - return isDefaultTypeIndexPattern(indexPattern); + isViable: indexPattern => { + return indexPattern && isDefaultTypeIndexPattern(indexPattern); }, }; -function msearch({ searchRequests, es, config, esShardTimeout }) { +function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, search_type: searchType, ignore_unavailable: true, - preference: getPreference(config) + preference: getPreference(config), }; const inlineBody = { ...body, - timeout: getTimeout(esShardTimeout) + timeout: getTimeout(esShardTimeout), }; return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`; }); @@ -58,11 +60,11 @@ function msearch({ searchRequests, es, config, esShardTimeout }) { }); return { searching: searching.then(({ responses }) => responses), - abort: searching.abort + abort: searching.abort, }; } -function search({ searchRequests, es, config, esShardTimeout }) { +function search({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); const promises = searchRequests.map(({ index, body }) => { diff --git a/src/legacy/ui/public/courier/search_strategy/index.js b/src/legacy/ui/public/courier/search_strategy/index.ts similarity index 100% rename from src/legacy/ui/public/courier/search_strategy/index.js rename to src/legacy/ui/public/courier/search_strategy/index.ts diff --git a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts similarity index 85% rename from src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js rename to src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts index 94c85c0e13ec7..3785ce6341078 100644 --- a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js +++ b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts @@ -17,7 +17,9 @@ * under the License. */ -export const isDefaultTypeIndexPattern = indexPattern => { +import { IndexPattern } from '../../../../core_plugins/data/public'; + +export const isDefaultTypeIndexPattern = (indexPattern: IndexPattern) => { // Default index patterns don't have `type` defined. return !indexPattern.type; }; diff --git a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts similarity index 79% rename from src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js rename to src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts index c4499cc870d56..24c3876cfcc05 100644 --- a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts @@ -17,21 +17,25 @@ * under the License. */ -import { SearchError } from './search_error'; import { i18n } from '@kbn/i18n'; +import { SearchError } from './search_error'; +import { SearchStrategyProvider } from './types'; -export const noOpSearchStrategy = { +export const noOpSearchStrategy: SearchStrategyProvider = { id: 'noOp', - search: async () => { + search: () => { const searchError = new SearchError({ status: '418', // "I'm a teapot" error title: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageTitle', { defaultMessage: 'No search strategy registered', }), - message: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', { - defaultMessage: `Couldn't find a search strategy for the search request`, - }), + message: i18n.translate( + 'common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', + { + defaultMessage: `Couldn't find a search strategy for the search request`, + } + ), type: 'NO_OP_SEARCH_STRATEGY', path: '', }); @@ -39,7 +43,6 @@ export const noOpSearchStrategy = { return { searching: Promise.reject(searchError), abort: () => {}, - failedSearchRequests: [], }; }, diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts b/src/legacy/ui/public/courier/search_strategy/search_error.d.ts deleted file mode 100644 index bf49853957c75..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type SearchError = any; -export type getSearchErrorType = any; diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.js b/src/legacy/ui/public/courier/search_strategy/search_error.ts similarity index 76% rename from src/legacy/ui/public/courier/search_strategy/search_error.js rename to src/legacy/ui/public/courier/search_strategy/search_error.ts index 9c35d11a6abf4..d4042fb17499c 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_error.js +++ b/src/legacy/ui/public/courier/search_strategy/search_error.ts @@ -17,8 +17,23 @@ * under the License. */ +interface SearchErrorOptions { + status: string; + title: string; + message: string; + path: string; + type: string; +} + export class SearchError extends Error { - constructor({ status, title, message, path, type }) { + public name: string; + public status: string; + public title: string; + public message: string; + public path: string; + public type: string; + + constructor({ status, title, message, path, type }: SearchErrorOptions) { super(message); this.name = 'SearchError'; this.status = status; @@ -39,9 +54,9 @@ export class SearchError extends Error { } } -export function getSearchErrorType({ message }) { +export function getSearchErrorType({ message }: Pick) { const msg = message.toLowerCase(); - if(msg.indexOf('unsupported query') > -1) { + if (msg.indexOf('unsupported query') > -1) { return 'UNSUPPORTED_QUERY'; } } diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts similarity index 58% rename from src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js rename to src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts index 362d303eb6203..ae2ed6128c8ea 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { IndexPattern } from '../../../../core_plugins/data/public'; import { noOpSearchStrategy } from './no_op_search_strategy'; import { searchStrategies, @@ -24,16 +25,28 @@ import { getSearchStrategyByViability, getSearchStrategyById, getSearchStrategyForSearchRequest, - hasSearchStategyForIndexPattern + hasSearchStategyForIndexPattern, } from './search_strategy_registry'; - -const mockSearchStrategies = [{ - id: 0, - isViable: index => index === 0 -}, { - id: 1, - isViable: index => index === 1 -}]; +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(() => { @@ -59,12 +72,16 @@ describe('Search strategy registry', () => { }); it('returns the viable strategy', () => { - expect(getSearchStrategyByViability(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyByViability(1)).toBe(mockSearchStrategies[1]); + 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(-1)).toBe(undefined); + expect(getSearchStrategyByViability({ id: '-1' } as IndexPattern)).toBe(undefined); }); }); @@ -74,12 +91,16 @@ describe('Search strategy registry', () => { }); it('returns the strategy by ID', () => { - expect(getSearchStrategyById(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyById(1)).toBe(mockSearchStrategies[1]); + 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); + expect(getSearchStrategyById('-1')).toBe(undefined); + }); + + it('returns the noOp search strategy if passed that ID', () => { + expect(getSearchStrategyById('noOp')).toBe(noOpSearchStrategy); }); }); @@ -89,15 +110,29 @@ describe('Search strategy registry', () => { }); it('returns the strategy by ID if provided', () => { - expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: 1 })).toBe(mockSearchStrategies[1]); + 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: 1 })).toBe(mockSearchStrategies[1]); + 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); + expect(getSearchStrategyForSearchRequest({ index: '3' })).toBe(noOpSearchStrategy); }); }); @@ -107,8 +142,8 @@ describe('Search strategy registry', () => { }); it('returns whether there is a search strategy for this index pattern', () => { - expect(hasSearchStategyForIndexPattern(0)).toBe(true); - expect(hasSearchStategyForIndexPattern(-1)).toBe(false); + expect(hasSearchStategyForIndexPattern({ id: '0' } as IndexPattern)).toBe(true); + expect(hasSearchStategyForIndexPattern({ id: '-1' } as IndexPattern)).toBe(false); }); }); }); diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts similarity index 64% rename from src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js rename to src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts index e67d39ea27aa6..9ef007f97531e 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts @@ -17,11 +17,14 @@ * under the License. */ +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { SearchStrategyProvider } from './types'; import { noOpSearchStrategy } from './no_op_search_strategy'; +import { SearchResponse } from '../types'; -export const searchStrategies = []; +export const searchStrategies: SearchStrategyProvider[] = []; -export const addSearchStrategy = searchStrategy => { +export const addSearchStrategy = (searchStrategy: SearchStrategyProvider) => { if (searchStrategies.includes(searchStrategy)) { return; } @@ -29,22 +32,27 @@ export const addSearchStrategy = searchStrategy => { searchStrategies.push(searchStrategy); }; -export const getSearchStrategyByViability = indexPattern => { +export const getSearchStrategyByViability = (indexPattern: IndexPattern) => { return searchStrategies.find(searchStrategy => { return searchStrategy.isViable(indexPattern); }); }; -export const getSearchStrategyById = searchStrategyId => { - return searchStrategies.find(searchStrategy => { +export const getSearchStrategyById = (searchStrategyId: string) => { + return [...searchStrategies, noOpSearchStrategy].find(searchStrategy => { return searchStrategy.id === searchStrategyId; }); }; -export const getSearchStrategyForSearchRequest = (searchRequest, { 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) { - return getSearchStrategyById(searchStrategyId); + const strategy = getSearchStrategyById(searchStrategyId); + if (!strategy) throw Error(`No strategy with ID ${searchStrategyId}`); + return strategy; } // Otherwise try to match it to a strategy. @@ -58,6 +66,6 @@ export const getSearchStrategyForSearchRequest = (searchRequest, { searchStrateg return noOpSearchStrategy; }; -export const hasSearchStategyForIndexPattern = indexPattern => { +export const hasSearchStategyForIndexPattern = (indexPattern: IndexPattern) => { return Boolean(getSearchStrategyByViability(indexPattern)); }; diff --git a/src/legacy/ui/public/courier/search_strategy/types.ts b/src/legacy/ui/public/courier/search_strategy/types.ts new file mode 100644 index 0000000000000..1542f9824a5b1 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/types.ts @@ -0,0 +1,37 @@ +/* + * 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 '../../../../core_plugins/data/public'; +import { FetchHandlers } from '../fetch/types'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface SearchStrategyProvider { + id: string; + search: (params: SearchStrategySearchParams) => SearchStrategyResponse; + isViable: (indexPattern: IndexPattern) => boolean; +} + +export interface SearchStrategyResponse { + searching: Promise; + abort: () => void; +} + +export interface SearchStrategySearchParams extends FetchHandlers { + searchRequests: SearchRequest[]; +} diff --git a/src/legacy/ui/public/courier/search_strategy/index.d.ts b/src/legacy/ui/public/courier/types.ts similarity index 84% rename from src/legacy/ui/public/courier/search_strategy/index.d.ts rename to src/legacy/ui/public/courier/types.ts index dc98484655d00..23d74ce6a57da 100644 --- a/src/legacy/ui/public/courier/search_strategy/index.d.ts +++ b/src/legacy/ui/public/courier/types.ts @@ -17,4 +17,7 @@ * under the License. */ -export { SearchError, getSearchErrorType } from './search_error'; +export * from './fetch/types'; +export * from './search_source/types'; +export * from './search_strategy/types'; +export * from './utils/types'; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts b/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts deleted file mode 100644 index 7f638d357a9e1..0000000000000 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts +++ /dev/null @@ -1,52 +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 { SearchSource } from 'ui/courier'; - -interface InspectorStat { - label: string; - value: string; - description: string; -} - -interface RequestInspectorStats { - indexPattern: InspectorStat; - indexPatternId: InspectorStat; -} - -interface ResponseInspectorStats { - queryTime: InspectorStat; - hitsTotal: InspectorStat; - hits: InspectorStat; - requestTime: InspectorStat; -} - -interface Response { - took: number; - hits: { - total: number; - hits: any[]; - }; -} - -export function getRequestInspectorStats(searchSource: SearchSource): RequestInspectorStats; -export function getResponseInspectorStats( - searchSource: SearchSource, - resp: Response -): ResponseInspectorStats; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts similarity index 78% rename from src/legacy/ui/public/courier/utils/courier_inspector_utils.js rename to src/legacy/ui/public/courier/utils/courier_inspector_utils.ts index 0e53f92bd9dcb..2c47fae4cce37 100644 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js +++ b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts @@ -25,51 +25,57 @@ */ import { i18n } from '@kbn/i18n'; +import { SearchResponse } from 'elasticsearch'; +import { SearchSourceContract, RequestInspectorStats } from '../types'; -function getRequestInspectorStats(searchSource) { - const stats = {}; +function getRequestInspectorStats(searchSource: SearchSourceContract) { + const stats: RequestInspectorStats = {}; const index = searchSource.getField('index'); if (index) { stats.indexPattern = { label: i18n.translate('common.ui.courier.indexPatternLabel', { - defaultMessage: 'Index pattern' + defaultMessage: 'Index pattern', }), value: index.title, description: i18n.translate('common.ui.courier.indexPatternDescription', { - defaultMessage: 'The index pattern that connected to the Elasticsearch indices.' + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', }), }; stats.indexPatternId = { label: i18n.translate('common.ui.courier.indexPatternIdLabel', { - defaultMessage: 'Index pattern ID' + defaultMessage: 'Index pattern ID', }), - value: index.id, + value: index.id!, description: i18n.translate('common.ui.courier.indexPatternIdDescription', { defaultMessage: 'The ID in the {kibanaIndexPattern} index.', - values: { kibanaIndexPattern: '.kibana' } + values: { kibanaIndexPattern: '.kibana' }, }), }; } return stats; } -function getResponseInspectorStats(searchSource, resp) { +function getResponseInspectorStats( + searchSource: SearchSourceContract, + resp: SearchResponse +) { const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; - const stats = {}; + const stats: RequestInspectorStats = {}; if (resp && resp.took) { stats.queryTime = { label: i18n.translate('common.ui.courier.queryTimeLabel', { - defaultMessage: 'Query time' + defaultMessage: 'Query time', }), value: i18n.translate('common.ui.courier.queryTimeValue', { defaultMessage: '{queryTime}ms', values: { queryTime: resp.took }, }), description: i18n.translate('common.ui.courier.queryTimeDescription', { - defaultMessage: 'The time it took to process the query. ' + - 'Does not include the time to send the request or parse it in the browser.' + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', }), }; } @@ -77,21 +83,21 @@ function getResponseInspectorStats(searchSource, resp) { if (resp && resp.hits) { stats.hitsTotal = { label: i18n.translate('common.ui.courier.hitsTotalLabel', { - defaultMessage: 'Hits (total)' + defaultMessage: 'Hits (total)', }), value: `${resp.hits.total}`, description: i18n.translate('common.ui.courier.hitsTotalDescription', { - defaultMessage: 'The number of documents that match the query.' + defaultMessage: 'The number of documents that match the query.', }), }; stats.hits = { label: i18n.translate('common.ui.courier.hitsLabel', { - defaultMessage: 'Hits' + defaultMessage: 'Hits', }), value: `${resp.hits.hits.length}`, description: i18n.translate('common.ui.courier.hitsDescription', { - defaultMessage: 'The number of documents returned by the query.' + defaultMessage: 'The number of documents returned by the query.', }), }; } @@ -99,15 +105,16 @@ function getResponseInspectorStats(searchSource, resp) { if (lastRequest && (lastRequest.ms === 0 || lastRequest.ms)) { stats.requestTime = { label: i18n.translate('common.ui.courier.requestTimeLabel', { - defaultMessage: 'Request time' + defaultMessage: 'Request time', }), value: i18n.translate('common.ui.courier.requestTimeValue', { defaultMessage: '{requestTime}ms', values: { requestTime: lastRequest.ms }, }), description: i18n.translate('common.ui.courier.requestTimeDescription', { - defaultMessage: 'The time of the request from the browser to Elasticsearch and back. ' + - 'Does not include the time the requested waited in the queue.' + defaultMessage: + 'The time of the request from the browser to Elasticsearch and back. ' + + 'Does not include the time the requested waited in the queue.', }), }; } diff --git a/src/legacy/ui/public/courier/index.js b/src/legacy/ui/public/courier/utils/types.ts similarity index 71% rename from src/legacy/ui/public/courier/index.js rename to src/legacy/ui/public/courier/utils/types.ts index 5647af3d0d645..305f27a86b398 100644 --- a/src/legacy/ui/public/courier/index.js +++ b/src/legacy/ui/public/courier/utils/types.ts @@ -17,12 +17,17 @@ * under the License. */ -export { SearchSource } from './search_source'; +export interface InspectorStat { + label: string; + value: string; + description: string; +} -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - isDefaultTypeIndexPattern, - SearchError, - getSearchErrorType, -} from './search_strategy'; +export interface RequestInspectorStats { + indexPattern?: InspectorStat; + indexPatternId?: InspectorStat; + queryTime?: InspectorStat; + hitsTotal?: InspectorStat; + hits?: InspectorStat; + requestTime?: InspectorStat; +} diff --git a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js b/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js deleted file mode 100644 index a15c602b7ba83..0000000000000 --- a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js +++ /dev/null @@ -1,126 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { fieldWildcardFilter, makeRegEx } from '../../field_wildcard'; - -describe('fieldWildcard', function () { - const metaFields = ['_id', '_type', '_source']; - - beforeEach(ngMock.module('kibana')); - - describe('makeRegEx', function () { - it('matches * in any position', function () { - expect('aaaaaabbbbbbbcccccc').to.match(makeRegEx('*a*b*c*')); - expect('a1234').to.match(makeRegEx('*1234')); - expect('1234a').to.match(makeRegEx('1234*')); - expect('12a34').to.match(makeRegEx('12a34')); - }); - - it('properly escapes regexp control characters', function () { - expect('account[user_id]').to.match(makeRegEx('account[*]')); - }); - - it('properly limits matches without wildcards', function () { - expect('username').to.match(makeRegEx('*name')); - expect('username').to.match(makeRegEx('user*')); - expect('username').to.match(makeRegEx('username')); - expect('username').to.not.match(makeRegEx('user')); - expect('username').to.not.match(makeRegEx('name')); - expect('username').to.not.match(makeRegEx('erna')); - }); - }); - - describe('filter', function () { - it('filters nothing when given undefined', function () { - const filter = fieldWildcardFilter(); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('filters nothing when given an empty array', function () { - const filter = fieldWildcardFilter([], metaFields); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('does not filter metaFields', function () { - const filter = fieldWildcardFilter([ '_*' ], metaFields); - - const original = [ - '_id', - '_type', - '_typefake' - ]; - - expect(original.filter(filter)).to.eql(['_id', '_type']); - }); - - it('filters values that match the globs', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4' - ], metaFields); - - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(['bar', 'baz']); - }); - - it('handles weird values okay', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4', - 'undefined' - ], metaFields); - - const original = [ - 'foo', - null, - 'bar', - undefined, - {}, - [], - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql([null, 'bar', {}, [], 'baz']); - }); - }); -}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts new file mode 100644 index 0000000000000..9f7523866fdc1 --- /dev/null +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { fieldWildcardFilter, makeRegEx } from './field_wildcard'; + +describe('fieldWildcard', () => { + const metaFields = ['_id', '_type', '_source']; + + describe('makeRegEx', function() { + it('matches * in any position', function() { + expect('aaaaaabbbbbbbcccccc').toMatch(makeRegEx('*a*b*c*')); + expect('a1234').toMatch(makeRegEx('*1234')); + expect('1234a').toMatch(makeRegEx('1234*')); + expect('12a34').toMatch(makeRegEx('12a34')); + }); + + it('properly escapes regexp control characters', function() { + expect('account[user_id]').toMatch(makeRegEx('account[*]')); + }); + + it('properly limits matches without wildcards', function() { + expect('username').toMatch(makeRegEx('*name')); + expect('username').toMatch(makeRegEx('user*')); + expect('username').toMatch(makeRegEx('username')); + expect('username').not.toMatch(makeRegEx('user')); + expect('username').not.toMatch(makeRegEx('name')); + expect('username').not.toMatch(makeRegEx('erna')); + }); + }); + + describe('filter', function() { + it('filters nothing when given undefined', function() { + const filter = fieldWildcardFilter(); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(val => filter(val))).toEqual(original); + }); + + it('filters nothing when given an empty array', function() { + const filter = fieldWildcardFilter([], metaFields); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(original); + }); + + it('does not filter metaFields', function() { + const filter = fieldWildcardFilter(['_*'], metaFields); + + const original = ['_id', '_type', '_typefake']; + + expect(original.filter(filter)).toEqual(['_id', '_type']); + }); + + it('filters values that match the globs', function() { + const filter = fieldWildcardFilter(['f*', '*4'], metaFields); + + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(['bar', 'baz']); + }); + + it('handles weird values okay', function() { + const filter = fieldWildcardFilter(['f*', '*4', 'undefined'], metaFields); + + const original = ['foo', null, 'bar', undefined, {}, [], 'baz', 1234]; + + expect(original.filter(filter)).toEqual([null, 'bar', {}, [], 'baz']); + }); + }); +}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.js b/src/legacy/ui/public/field_wildcard/field_wildcard.ts similarity index 70% rename from src/legacy/ui/public/field_wildcard/field_wildcard.js rename to src/legacy/ui/public/field_wildcard/field_wildcard.ts index 656641b20a98c..5437086ddd6f4 100644 --- a/src/legacy/ui/public/field_wildcard/field_wildcard.js +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.ts @@ -19,25 +19,29 @@ import { escapeRegExp, memoize } from 'lodash'; -export const makeRegEx = memoize(function makeRegEx(glob) { - return new RegExp('^' + glob.split('*').map(escapeRegExp).join('.*') + '$'); +export const makeRegEx = memoize(function makeRegEx(glob: string) { + const globRegex = glob + .split('*') + .map(escapeRegExp) + .join('.*'); + return new RegExp(`^${globRegex}$`); }); // Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardMatcher(globs = [], metaFields) { - return function matcher(val) { +export function fieldWildcardMatcher(globs: string[] = [], metaFields: unknown[] = []) { + return function matcher(val: unknown) { // do not test metaFields or keyword if (metaFields.indexOf(val) !== -1) { return false; } - return globs.some(p => makeRegEx(p).test(val)); + return globs.some(p => makeRegEx(p).test(`${val}`)); }; } // Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardFilter(globs = [], metaFields = []) { +export function fieldWildcardFilter(globs: string[] = [], metaFields: string[] = []) { const matcher = fieldWildcardMatcher(globs, metaFields); - return function filter(val) { + return function filter(val: unknown) { return !matcher(val); }; } diff --git a/src/legacy/ui/public/field_wildcard/index.js b/src/legacy/ui/public/field_wildcard/index.ts similarity index 100% rename from src/legacy/ui/public/field_wildcard/index.js rename to src/legacy/ui/public/field_wildcard/index.ts diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts index 8ef97c0b3ebcc..3d435f2ba8dfd 100644 --- a/src/legacy/ui/public/promises/defer.ts +++ b/src/legacy/ui/public/promises/defer.ts @@ -17,7 +17,7 @@ * under the License. */ -interface Defer { +export interface Defer { promise: Promise; resolve(value: T): void; reject(reason: Error): void; diff --git a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx index ebbe886b3650b..19cbbf9cea04c 100644 --- a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx +++ b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx @@ -19,7 +19,7 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; -import { SearchError } from 'ui/courier'; +import { SearchError } from '../../courier'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; interface VisualizationRequestErrorProps { @@ -32,7 +32,7 @@ export class VisualizationRequestError extends React.Component diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts index 70e0c1f1382fa..608a8b9ce8aa7 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -28,7 +28,7 @@ import { } from './build_pipeline'; import { Vis, VisState } from 'ui/vis'; import { AggConfig } from 'ui/agg_types/agg_config'; -import { searchSourceMock } from 'ui/courier/search_source/mocks'; +import { searchSourceMock } from '../../../courier/search_source/mocks'; jest.mock('ui/new_platform'); jest.mock('ui/agg_types/buckets/date_histogram', () => ({ diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 21b13abea440e..ca9540b4d3737 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -20,11 +20,11 @@ import { cloneDeep, get } from 'lodash'; // @ts-ignore import { setBounds } from 'ui/agg_types'; -import { SearchSource } from 'ui/courier'; import { AggConfig, Vis, VisParams, VisState } from 'ui/vis'; import { isDateHistogramBucketAggConfig } from 'ui/agg_types/buckets/date_histogram'; import moment from 'moment'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../courier/types'; import { createFormat } from './utilities'; interface SchemaConfigParams { @@ -462,7 +462,7 @@ export const buildVislibDimensions = async ( // take a Vis object and decorate it with the necessary params (dimensions, bucket, metric, etc) export const getVisParams = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any; abortSignal?: AbortSignal } + params: { searchSource: SearchSourceContract; timeRange?: any; abortSignal?: AbortSignal } ) => { const schemas = getSchemas(vis, params.timeRange); let visConfig = cloneDeep(vis.params); @@ -479,7 +479,10 @@ export const getVisParams = async ( export const buildPipeline = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any } + params: { + searchSource: SearchSourceContract; + timeRange?: any; + } ) => { const { searchSource } = params; const { indexPattern } = vis; diff --git a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts index 36759551a1723..a9203415321fa 100644 --- a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts +++ b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts @@ -24,13 +24,13 @@ import { toastNotifications } from 'ui/notify'; import { AggConfig } from 'ui/vis'; import { timefilter } from 'ui/timefilter'; import { Vis } from '../../../vis'; +import { SearchSource, SearchSourceContract } from '../../../courier'; import { esFilters, Query } from '../../../../../../plugins/data/public'; -import { SearchSource } from '../../../courier'; interface QueryGeohashBoundsParams { filters?: esFilters.Filter[]; query?: Query; - searchSource?: SearchSource; + searchSource?: SearchSourceContract; } /** diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6a5c7bdf8eea3..6e03c665290ae 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -49,4 +49,11 @@ describe('filterMatchesIndex', () => { expect(filterMatchesIndex(filter, indexPattern)).toBe(false); }); + + it('should return true if the filter has meta without a key', () => { + const filter = { meta: { index: 'foo' } } as Filter; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + }); }); diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 496aab3ea585f..9b68f5088c447 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -26,7 +26,7 @@ import { Filter } from '../filters'; * change. */ export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { - if (!filter.meta || !indexPattern) { + if (!filter.meta?.key || !indexPattern) { return true; } return indexPattern.fields.some((field: IFieldType) => field.name === filter.meta.key); diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index fa07b3e611fa7..3d819bd145fa6 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -63,18 +63,22 @@ export type RangeFilterMeta = FilterMeta & { formattedValue?: string; }; -export type RangeFilter = Filter & { - meta: RangeFilterMeta; - script?: { - script: { - params: any; - lang: string; - source: any; +export interface EsRangeFilter { + range: { [key: string]: RangeFilterParams }; +} + +export type RangeFilter = Filter & + EsRangeFilter & { + meta: RangeFilterMeta; + script?: { + script: { + params: any; + lang: string; + source: any; + }; }; + match_all?: any; }; - match_all?: any; - range: { [key: string]: RangeFilterParams }; -}; export const isRangeFilter = (filter: any): filter is RangeFilter => filter && filter.range; diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts index 5312f1be6c26c..8788d4b690aba 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts @@ -20,36 +20,19 @@ import { getHighlightRequest } from './highlight_request'; describe('getHighlightRequest', () => { - let configMock: Record; - const getConfig = (key: string) => configMock[key]; const queryStringQuery = { query_string: { query: 'foo' } }; - beforeEach(function() { - configMock = {}; - configMock['doc_table:highlight'] = true; - }); - test('should be a function', () => { expect(getHighlightRequest).toBeInstanceOf(Function); }); test('should not modify the original query', () => { - getHighlightRequest(queryStringQuery, getConfig); + getHighlightRequest(queryStringQuery, true); expect(queryStringQuery.query_string).not.toHaveProperty('highlight'); }); test('should return undefined if highlighting is turned off', () => { - configMock['doc_table:highlight'] = false; - const request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).toBe(undefined); - }); - - test('should enable/disable highlighting if config is changed', () => { - let request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).not.toBe(undefined); - - configMock['doc_table:highlight'] = false; - request = getHighlightRequest(queryStringQuery, getConfig); + const request = getHighlightRequest(queryStringQuery, false); expect(request).toBe(undefined); }); }); diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts index 199a73e692e39..8012ab59c33ba 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts @@ -21,8 +21,8 @@ import { highlightTags } from './highlight_tags'; const FRAGMENT_SIZE = Math.pow(2, 31) - 1; // Max allowed value for fragment_size (limit of a java int) -export function getHighlightRequest(query: any, getConfig: Function) { - if (!getConfig('doc_table:highlight')) return; +export function getHighlightRequest(query: any, shouldHighlight: boolean) { + if (!shouldHighlight) return; return { pre_tags: [highlightTags.pre], diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a1eb36c2ee028..a8eb3a3fe8102 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import sinon from 'sinon'; -import { Filter, getTime } from './get_time'; +import { getTime } from './get_time'; describe('get_time', () => { describe('getTime', () => { @@ -43,8 +43,8 @@ describe('get_time', () => { ], } as any, { from: 'now-60y', to: 'now' } - ) as Filter; - expect(filter.range.date).toEqual({ + ); + expect(filter!.range.date).toEqual({ gte: '1940-02-01T00:00:00.000Z', lte: '2000-02-01T00:00:00.000Z', format: 'strict_date_optional_time', diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index 41ad1a49af0ff..d3fbc17734f81 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -21,22 +21,13 @@ import dateMath from '@elastic/datemath'; import { TimeRange } from '../../../common'; // TODO: remove this -import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public/index_patterns'; +import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public'; +import { esFilters } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; } -interface RangeFilter { - gte?: string | number; - lte?: string | number; - format: string; -} - -export interface Filter { - range: { [s: string]: RangeFilter }; -} - export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOptions = {}) { return { min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }), @@ -45,10 +36,10 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp } export function getTime( - indexPattern: IndexPattern, + indexPattern: IndexPattern | undefined, timeRange: TimeRange, forceNow?: Date -): Filter | undefined { +) { if (!indexPattern) { // in CI, we sometimes seem to fail here. return; @@ -66,17 +57,13 @@ export function getTime( if (!bounds) { return; } - const filter: Filter = { - range: { [timefield.name]: { format: 'strict_date_optional_time' } }, - }; - - if (bounds.min) { - filter.range[timefield.name].gte = bounds.min.toISOString(); - } - - if (bounds.max) { - filter.range[timefield.name].lte = bounds.max.toISOString(); - } - - return filter; + return esFilters.buildRangeFilter( + timefield, + { + ...(bounds.min && { gte: bounds.min.toISOString() }), + ...(bounds.max && { lte: bounds.max.toISOString() }), + format: 'strict_date_optional_time', + }, + indexPattern + ); } diff --git a/x-pack/legacy/plugins/infra/types/eui.d.ts b/x-pack/legacy/plugins/infra/types/eui.d.ts index 2907830ff882f..7cf0a91e88c1f 100644 --- a/x-pack/legacy/plugins/infra/types/eui.d.ts +++ b/x-pack/legacy/plugins/infra/types/eui.d.ts @@ -68,6 +68,7 @@ declare module '@elastic/eui' { rowProps?: any; cellProps?: any; responsive?: boolean; + itemIdToExpandedRowMap?: any; }; export const EuiInMemoryTable: React.FC; } diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 12fab24d1f8d6..7169014542710 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import { getRequestInspectorStats, getResponseInspectorStats } from '../../../../../src/legacy/ui/public/courier'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { esFilters } from '../../../../../src/plugins/data/public'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; -export { SearchSource } from 'ui/courier'; +export { SearchSource } from '../../../../../src/legacy/ui/public/courier'; export const indexPatternService = data.indexPatterns.indexPatterns; export async function fetchSearchSourceAndRecordWithInspector({ diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts index 2e442c5c61b1e..2bff760ed3711 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -5,11 +5,12 @@ */ import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; +import { SearchSourceContract } from '../../../../../../../../../src/legacy/ui/public/courier'; export const savedSearchMock = { id: 'the-saved-search-id', title: 'the-saved-search-title', - searchSource: searchSourceMock, + searchSource: searchSourceMock as SearchSourceContract, columns: [], sort: [], destroy: () => {}, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 1caa068620618..642b4c5649a13 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -176,7 +176,7 @@ export const Page: FC = () => { const searchSource = currentSavedSearch.searchSource; const query = searchSource.getField('query'); if (query !== undefined) { - const queryLanguage = query.language; + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; const qryString = query.query; let qry; if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 0e88b291e76fc..455fac9b532d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -7,7 +7,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { esQuery, Query, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; export interface SearchItems { indexPattern: IIndexPattern; @@ -28,7 +28,7 @@ export function createSearchItems( // a lucene query_string. // Using a blank query will cause match_all:{} to be used // when passed through luceneStringToDsl - let query = { + let query: Query = { query: '', language: 'lucene', }; @@ -45,12 +45,12 @@ export function createSearchItems( if (indexPattern.id === undefined && savedSearch.id !== undefined) { const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index'); + indexPattern = searchSource.getField('index')!; - query = searchSource.getField('query'); + query = searchSource.getField('query')!; const fs = searchSource.getField('filter'); - const filters = fs.length ? fs : []; + const filters = Array.isArray(fs) ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index a614be547abde..aeec71462308e 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -38,7 +38,7 @@ export function loadNewJobCapabilities( // saved search is being used // load the index pattern from the saved search const savedSearch = await savedSearches.get(savedSearchId); - const indexPattern = savedSearch.searchSource.getField('index'); + const indexPattern = savedSearch.searchSource.getField('index')!; await newJobCapsService.initializeFromIndexPattern(indexPattern); resolve(newJobCapsService.newJobCaps); } else { diff --git a/x-pack/legacy/plugins/rollup/public/search/register.js b/x-pack/legacy/plugins/rollup/public/search/register.js index 917ee872254f5..f7f1c681b63ca 100644 --- a/x-pack/legacy/plugins/rollup/public/search/register.js +++ b/x-pack/legacy/plugins/rollup/public/search/register.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { addSearchStrategy } from 'ui/courier'; +import { addSearchStrategy } from '../../../../../../src/legacy/ui/public/courier'; import { rollupSearchStrategy } from './rollup_search_strategy'; export function initSearch() { diff --git a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js index ab24a37a2ecec..28f08ba1ab952 100644 --- a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js +++ b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js @@ -5,7 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { SearchError, getSearchErrorType } from 'ui/courier'; +import { SearchError, getSearchErrorType } from '../../../../../../src/legacy/ui/public/courier'; function serializeFetchParams(searchRequests) { return JSON.stringify(searchRequests.map(searchRequestWithFetchParams => { From a38ff621b6409f7662b5f4555af3315f6a1963e8 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 25 Nov 2019 21:26:51 -0500 Subject: [PATCH 26/52] Add types for Embeddable setup and start contracts (#51654) --- .../kibana/public/dashboard/application.ts | 4 ++-- .../core_plugins/kibana/public/dashboard/plugin.ts | 6 +++--- .../core_plugins/kibana/public/discover/plugin.ts | 9 +++------ src/legacy/ui/public/new_platform/new_platform.ts | 6 +++--- .../public/actions/open_replace_panel_flyout.tsx | 4 ++-- .../public/actions/replace_panel_action.tsx | 4 ++-- .../public/actions/replace_panel_flyout.tsx | 11 +++-------- .../public/embeddable/dashboard_container.tsx | 4 ++-- .../dashboard_embeddable_container/public/plugin.tsx | 6 +++--- src/plugins/embeddable/public/index.ts | 3 +-- src/plugins/embeddable/public/mocks.ts | 9 +++++---- src/plugins/embeddable/public/plugin.ts | 11 +++++++---- src/plugins/embeddable/public/tests/test_plugin.ts | 6 +++--- .../public/np_ready/public/plugin.tsx | 10 +++++++--- .../lens/public/editor_frame_plugin/plugin.tsx | 8 ++++---- x-pack/plugins/advanced_ui_actions/public/plugin.ts | 8 ++++---- 16 files changed, 54 insertions(+), 55 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/application.ts index 57391223fa147..9c50adeeefccb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/application.ts @@ -48,7 +48,7 @@ import { // @ts-ignore import { initDashboardApp } from './legacy_app'; import { DataStart } from '../../../data/public'; -import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationStart } from '../../../navigation/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; @@ -68,7 +68,7 @@ export interface RenderDeps { chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: DataStart['search']['services']['savedQueryService']; - embeddables: ReturnType; + embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index deb291deb0d5a..609bd717f3c48 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n'; import { RenderDeps } from './application'; import { DataStart } from '../../../data/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; -import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationStart } from '../../../navigation/public'; import { DashboardConstants } from './dashboard_constants'; @@ -49,7 +49,7 @@ export interface LegacyAngularInjectedDependencies { export interface DashboardPluginStartDependencies { data: DataStart; npData: NpDataStart; - embeddables: ReturnType; + embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; } @@ -67,7 +67,7 @@ export class DashboardPlugin implements Plugin { dataStart: DataStart; npDataStart: NpDataStart; savedObjectsClient: SavedObjectsClientContract; - embeddables: ReturnType; + embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; } | null = null; diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 873c429bf705d..7c2fb4f118915 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -21,10 +21,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/p import { IUiActionsStart } from 'src/plugins/ui_actions/public'; import { registerFeature } from './helpers/register_feature'; import './kibana_services'; -import { - Start as EmbeddableStart, - Setup as EmbeddableSetup, -} from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; /** * These are the interfaces with your public contracts. You should export these @@ -35,11 +32,11 @@ export type DiscoverSetup = void; export type DiscoverStart = void; interface DiscoverSetupPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; } interface DiscoverStartPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; } export class DiscoverPlugin implements Plugin { diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 36bfbcc7d5d46..c0b2d6d913257 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -19,7 +19,7 @@ import { IScope } from 'angular'; import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; -import { Start as EmbeddableStart, Setup as EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; @@ -35,7 +35,7 @@ import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/pu export interface PluginsSetup { data: ReturnType; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; @@ -47,7 +47,7 @@ export interface PluginsSetup { export interface PluginsStart { data: ReturnType; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; eui_utils: EuiUtilsStart; expressions: ReturnType; home: HomePublicPluginStart; diff --git a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx index b30733760bbdf..f15d538703e21 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - Start as EmbeddableStart, + IEmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx index f6d2fcbcd57fd..78ce6bdc4c58f 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, Start as EmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { IAction, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements IAction { private core: CoreStart, private savedobjectfinder: React.ComponentType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ActionContext) { diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx index 36efd0bcba676..36313353e3c33 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx @@ -20,15 +20,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; -import { - IContainer, - IEmbeddable, - EmbeddableInput, - EmbeddableOutput, - Start as EmbeddableStart, -} from '../embeddable_plugin'; +import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; interface Props { container: IContainer; @@ -36,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: GetEmbeddableFactories; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx index 6cefd11c912f1..684aa93779bc1 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - Start as EmbeddableStartContract, + IEmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: EmbeddableStartContract; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index dbb5a06da9cd9..79cc9b6980545 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { IUiActionsSetup, IUiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -34,12 +34,12 @@ import { } from '../../../plugins/kibana_react/public'; interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; uiActions: IUiActionsStart; } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 33855b07df7a1..ea2bd910b0624 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -61,5 +61,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { EmbeddablePublicPlugin as Plugin }; -export * from './plugin'; +export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index ef1517bb7f1d5..fd299bc626fb9 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,14 +17,15 @@ * under the License. */ -import { Plugin } from '.'; +import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked>; -export type Start = jest.Mocked>; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -43,7 +44,7 @@ const createStartContract = (): Start => { }; const createInstance = () => { - const plugin = new Plugin({} as any); + const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: uiActionsPluginMock.createSetupContract(), }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index 458c8bfeb8762..df1f4e5080031 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -27,7 +27,13 @@ export interface IEmbeddableSetupDependencies { uiActions: IUiActionsSetup; } -export class EmbeddablePublicPlugin implements Plugin { +export interface IEmbeddableSetup { + registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +} + +export type IEmbeddableStart = EmbeddableApi; + +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private api!: EmbeddableApi; @@ -52,6 +58,3 @@ export class EmbeddablePublicPlugin implements Plugin { public stop() {} } - -export type Setup = ReturnType; -export type Start = ReturnType; diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 5b50bddefcdb7..6d1e15137480a 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -21,14 +21,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; // eslint-disable-next-line import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests'; import { IUiActionsApi } from 'src/plugins/ui_actions/public'; -import { EmbeddablePublicPlugin } from '../plugin'; +import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: ReturnType; - doStart: (anotherCoreStart?: CoreStart) => ReturnType; + setup: IEmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; uiActions: IUiActionsApi; } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index f03b3c4a1e0a5..6b82a67b9fcda 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -27,7 +27,7 @@ import { Setup as InspectorSetupContract, } from '../../../../../../../src/plugins/inspector/public'; -import { Plugin as EmbeddablePlugin, CONTEXT_MENU_TRIGGER } from './embeddable_api'; +import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; @@ -38,9 +38,13 @@ import { ContactCardEmbeddableFactory, } from './embeddable_api'; import { App } from './app'; +import { + IEmbeddableStart, + IEmbeddableSetup, +} from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { SavedObjectFinder: React.ComponentType; @@ -49,7 +53,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; inspector: InspectorStartContract; __LEGACY: { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 354a5186db4c1..f7399255b2001 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -15,8 +15,8 @@ import { ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; import { - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../../../src/plugins/embeddable/public'; import { setup as dataSetup, @@ -36,13 +36,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: typeof dataSetup; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: typeof dataStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; expressions: ExpressionsStart; chrome: Chrome; } diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index e2d1892b1355e..cc4a7c90de513 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -15,8 +15,8 @@ import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_act import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction } from './custom_time_range_action'; @@ -24,12 +24,12 @@ import { CustomTimeRangeBadge } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; interface SetupDependencies { - embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. + embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; } From d158fd8881ad98b18a4f5233ffeed85a663402fa Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Nov 2019 19:27:08 -0700 Subject: [PATCH 27/52] [Maps] only provide visiblity check when vector layer has joins (#51388) * [Maps] only provide visiblity check when vector layer has joins * clean up * properly set line filter expression * use ternary statements instead of if * review feedback --- .../legacy/plugins/maps/common/constants.js | 2 +- .../layers/util/mb_filter_expressions.js | 62 +++++++++++++++ .../maps/public/layers/vector_layer.js | 77 ++++++++----------- x-pack/test/functional/apps/maps/joins.js | 4 +- .../functional/apps/maps/mapbox_styles.js | 6 +- 5 files changed, 102 insertions(+), 49 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 691c679e5290b..3b2f887e13c87 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -68,7 +68,7 @@ export const ZOOM_PRECISION = 2; export const ES_SIZE_LIMIT = 10000; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; -export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__'; +export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js new file mode 100644 index 0000000000000..393c290d69668 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js @@ -0,0 +1,62 @@ +/* + * 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 { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; + +const VISIBILITY_FILTER_CLAUSE = ['all', + [ + '==', + ['get', FEATURE_VISIBLE_PROPERTY_NAME], + true + ] +]; + +const CLOSED_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] +]; + +const VISIBLE_CLOSED_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + CLOSED_SHAPE_MB_FILTER, +]; + +const ALL_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] +]; + +const VISIBLE_ALL_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + ALL_SHAPE_MB_FILTER, +]; + +const POINT_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] +]; + +const VISIBLE_POINT_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + POINT_MB_FILTER, +]; + +export function getFillFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER; +} + +export function getLineFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_ALL_SHAPE_MB_FILTER : ALL_SHAPE_MB_FILTER; +} + +export function getPointFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index e6b07b983d898..9b553803606ed 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -10,7 +10,6 @@ import { AbstractLayer } from './layer'; import { VectorStyle } from './styles/vector/vector_style'; import { InnerJoin } from './joins/inner_join'; import { - GEO_JSON_TYPE, FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, @@ -24,41 +23,11 @@ import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; import { assignFeatureIds } from './util/assign_feature_ids'; - -const VISIBILITY_FILTER_CLAUSE = ['all', - [ - '==', - ['get', FEATURE_VISIBLE_PROPERTY_NAME], - true - ] -]; - -const FILL_LAYER_MB_FILTER = [ - ...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] - ] -]; - -const LINE_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] - ] -]; - -const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] - ] -]; +import { + getFillFilterExpression, + getLineFilterExpression, + getPointFilterExpression, +} from './util/mb_filter_expressions'; export class VectorLayer extends AbstractLayer { @@ -107,6 +76,10 @@ export class VectorLayer extends AbstractLayer { }); } + _hasJoins() { + return this.getValidJoins().length > 0; + } + isDataLoaded() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { @@ -495,12 +468,14 @@ export class VectorLayer extends AbstractLayer { } const sourceResult = await this._syncSource(syncContext); - if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) { + if ( + !sourceResult.featureCollection || + !sourceResult.featureCollection.features.length || + !this._hasJoins()) { return; } const joinStates = await this._syncJoins(syncContext); - await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } @@ -571,7 +546,11 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(pointLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(pointLayerId)) { + mbMap.setFilter(pointLayerId, filterExpr); } this._style.setMBPaintPropertiesForPoints({ @@ -592,7 +571,11 @@ export class VectorLayer extends AbstractLayer { type: 'symbol', source: sourceId, }); - mbMap.setFilter(symbolLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(symbolLayerId)) { + mbMap.setFilter(symbolLayerId, filterExpr); } this._style.setMBSymbolPropertiesForPoints({ @@ -606,6 +589,7 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); + const hasJoins = this._hasJoins(); if (!mbMap.getLayer(fillLayerId)) { mbMap.addLayer({ id: fillLayerId, @@ -613,7 +597,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(fillLayerId, FILL_LAYER_MB_FILTER); } if (!mbMap.getLayer(lineLayerId)) { mbMap.addLayer({ @@ -622,7 +605,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(lineLayerId, LINE_LAYER_MB_FILTER); } this._style.setMBPaintProperties({ alpha: this.getAlpha(), @@ -632,9 +614,18 @@ export class VectorLayer extends AbstractLayer { }); this.syncVisibilityWithMb(mbMap, fillLayerId); + mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const fillFilterExpr = getFillFilterExpression(hasJoins); + if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) { + mbMap.setFilter(fillLayerId, fillFilterExpr); + } + this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); - mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const lineFilterExpr = getLineFilterExpression(hasJoins); + if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { + mbMap.setFilter(lineLayerId, lineFilterExpr); + } } _syncStylePropertiesWithMb(mbMap) { diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 1634bea47a69f..30b957fdf45f4 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, true, true]); @@ -166,7 +166,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, false, false]); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 49519b530337e..bfa4be2b067af 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -15,7 +15,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -89,7 +89,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -160,7 +160,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ From 320fa5a550953f672d3293bdc2532a6981d4c87d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 25 Nov 2019 20:58:35 -0700 Subject: [PATCH 28/52] Default search params for ignore_unavailable and rest_total_hits_as_int (#50953) * Default search params for ignore_unavailable and rest_total_hits_as_int * Update code with options arg * Fix filter matches index for filters with partial meta * Fix tests --- src/plugins/data/public/search/i_search.ts | 4 +-- .../public/search/sync_search_strategy.ts | 2 +- .../data/server/search/create_api.test.ts | 2 +- src/plugins/data/server/search/create_api.ts | 2 +- .../es_search/es_search_strategy.test.ts | 28 +++++++++++++++++-- .../search/es_search/es_search_strategy.ts | 17 +++++------ src/plugins/data/server/search/i_search.ts | 8 +++++- src/plugins/data/server/search/routes.test.ts | 4 +-- src/plugins/data/server/search/routes.ts | 2 +- .../data/server/search/search_service.ts | 2 +- .../plugins/demo_search/server/constants.ts | 20 ------------- 11 files changed, 51 insertions(+), 40 deletions(-) delete mode 100644 test/plugin_functional/plugins/demo_search/server/constants.ts diff --git a/src/plugins/data/public/search/i_search.ts b/src/plugins/data/public/search/i_search.ts index 0e256b960ffa3..a39ef3e3e7571 100644 --- a/src/plugins/data/public/search/i_search.ts +++ b/src/plugins/data/public/search/i_search.ts @@ -49,11 +49,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], - options: ISearchOptions, + options?: ISearchOptions, strategy?: T ) => Observable; export type ISearch = ( request: IRequestTypesMap[T], - options: ISearchOptions + options?: ISearchOptions ) => Observable; diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index c412bbb3b104a..3885a97a98571 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -34,7 +34,7 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { const search: ISearch = ( request: ISyncSearchRequest, - options: ISearchOptions + options: ISearchOptions = {} ) => { const response: Promise = context.core.http.fetch( `/internal/search/${request.serverStrategy}`, diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts index 32570a05031f6..cc13269e1aa21 100644 --- a/src/plugins/data/server/search/create_api.test.ts +++ b/src/plugins/data/server/search/create_api.test.ts @@ -55,7 +55,7 @@ describe('createApi', () => { }); it('should throw if no provider is found for the given name', () => { - expect(api.search({}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( + expect(api.search({}, {}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( `"No strategy found for noneByThisName"` ); }); diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index 4c13dd9e1137c..2a874869526d7 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -30,7 +30,7 @@ export function createApi({ caller: APICaller; }) { const api: IRouteHandlerSearchContext = { - search: async (request, strategyName) => { + search: async (request, options, strategyName) => { const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; const strategyProvider = searchStrategies[name]; if (!strategyProvider) { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 619a28df839bd..7b725a47aa13b 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -66,7 +66,7 @@ describe('ES search strategy', () => { expect(spy).toBeCalled(); }); - it('calls the API caller with the params', () => { + it('calls the API caller with the params with defaults', () => { const params = { index: 'logstash-*' }; const esSearch = esSearchStrategyProvider( { @@ -80,7 +80,31 @@ describe('ES search strategy', () => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); - expect(mockApiCaller.mock.calls[0][1]).toEqual(params); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + ignoreUnavailable: true, + restTotalHitsAsInt: true, + }); + }); + + it('calls the API caller with overridden defaults', () => { + const params = { index: 'logstash-*', ignoreUnavailable: false }; + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + esSearch.search({ params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('search'); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + restTotalHitsAsInt: true, + }); }); it('returns total, loaded, and raw response', async () => { 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 31f4fc15a0989..c5fc1d9d3a11c 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 @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../../common/search'; +import { ES_SEARCH_STRATEGY } from '../../../common/search'; import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy'; import { ISearchContext } from '..'; @@ -27,16 +27,17 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { - search: async (request: IEsSearchRequest) => { + search: async (request, options) => { + const params = { + ignoreUnavailable: true, // Don't fail if the index/indices don't exist + restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range + ...request.params, + }; if (request.debug) { // eslint-disable-next-line - console.log(JSON.stringify(request, null, 2)); + console.log(JSON.stringify(params, null, 2)); } - const esSearchResponse = (await caller('search', { - ...request.params, - // TODO: could do something like this here? - // ...getCurrentSearchParams(context), - })) as SearchResponse; + const esSearchResponse = (await caller('search', params, options)) as SearchResponse; // The above query will either complete or timeout and throw an error. // There is no progress indication on this api. diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index fabcb98ceea72..0a35734574153 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -22,6 +22,10 @@ import { TStrategyTypes } from './strategy_types'; import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search'; import { IEsSearchRequest } from './es_search'; +export interface ISearchOptions { + signal?: AbortSignal; +} + export interface IRequestTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchRequest; [key: string]: IKibanaSearchRequest; @@ -34,9 +38,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], + options?: ISearchOptions, strategy?: T ) => Promise; export type ISearch = ( - request: IRequestTypesMap[T] + request: IRequestTypesMap[T], + options?: ISearchOptions ) => Promise; diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index ebdcf48f608b9..a2394d88f3931 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -60,7 +60,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); @@ -92,7 +92,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.internalError).toBeCalled(); expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); }); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 6cb6c28c76014..eaa72548e08ee 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -36,7 +36,7 @@ export function registerSearchRoute(router: IRouter): void { const searchRequest = request.body; const strategy = request.params.strategy; try { - const response = await context.search!.search(searchRequest, strategy); + const response = await context.search!.search(searchRequest, {}, strategy); return res.ok({ body: response }); } catch (err) { return res.internalError({ body: err }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 4edb51300dfaf..3409a72326121 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -77,7 +77,7 @@ export class SearchService implements Plugin { caller, searchStrategies: this.searchStrategies, }); - return searchAPI.search(request, strategyName); + return searchAPI.search(request, {}, strategyName); }, }, }; diff --git a/test/plugin_functional/plugins/demo_search/server/constants.ts b/test/plugin_functional/plugins/demo_search/server/constants.ts deleted file mode 100644 index 11c258a21d5a8..0000000000000 --- a/test/plugin_functional/plugins/demo_search/server/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const FAKE_PROGRESS_STRATEGY = 'FAKE_PROGRESS_STRATEGY'; From 1e6f9d54fe1155f0766515ba2ece02a7828b8a71 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 26 Nov 2019 04:04:31 +0000 Subject: [PATCH 29/52] chore(NA): ability to add manual exceptions for the clean dll logic on the build (#51642) --- .../clean_client_modules_on_dll_task.js | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 5c0462ce86fa9..b0e38b6481457 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -66,10 +66,38 @@ export const CleanClientModulesOnDLLTask = { // side code entries that were provided const serverDependencies = await getDependencies(baseDir, serverEntries); + // This fulfill a particular exceptional case where + // we need to keep loading a file from a node_module + // only used in the front-end like we do when using the file-loader + // in https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js + // + // manual list of exception modules + const manualExceptionModules = [ + 'mapbox-gl' + ]; + + // consider the top modules as exceptions as the entry points + // to look for other exceptions dependent on that one + const manualExceptionEntries = [ + ...manualExceptionModules.map(module => `${baseDir}/node_modules/${module}`) + ]; + + // dependencies for declared exception modules + const manualExceptionModulesDependencies = await getDependencies(baseDir, [ + ...manualExceptionEntries + ]); + + // final list of manual exceptions to add + const manualExceptions = [ + ...manualExceptionModules, + ...manualExceptionModulesDependencies + ]; + // Consider this as our whiteList for the modules we can't delete const whiteListedModules = [ ...serverDependencies, - ...kbnWebpackLoaders + ...kbnWebpackLoaders, + ...manualExceptions ]; // Resolve the client vendors dll manifest path From c4141fa62bcd7b5a7aa60dd6e7c088237e4a2c10 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 26 Nov 2019 00:44:26 -0700 Subject: [PATCH 30/52] skip flaky suite (#45321) --- x-pack/test/functional/apps/graph/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index cb6f0b6028a2d..f640a34b36ddf 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -13,7 +13,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('graph', function() { + // FLAKY: https://github.com/elastic/kibana/issues/45321 + describe.skip('graph', function() { before(async () => { await browser.setWindowSize(1600, 1000); log.debug('load graph/secrepo data'); From e753c35b9d37b32717aaa5fc0555e506253c40d2 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 26 Nov 2019 09:16:05 +0100 Subject: [PATCH 31/52] performs logout using the API (#51596) --- .../plugins/siem/cypress/integration/lib/logout/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts index 132242606d88c..7a6c7f71bc98c 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOGOUT } from '../urls'; - export const logout = (): null => { - cy.visit(`${Cypress.config().baseUrl}${LOGOUT}`); + cy.request({ + method: 'GET', + url: `${Cypress.config().baseUrl}/logout`, + }).then(response => { + expect(response.status).to.eq(200); + }); return null; }; From 7fef618ea6a1d370240ed10359f6321f56d11279 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 26 Nov 2019 10:21:49 +0100 Subject: [PATCH 32/52] Add compatibility wrapper for Boom errors thrown from route handler (#51157) * add wrapErrors method to router * add RouteRegistrar type * update generated doc * add migration example * rename wrapErrors to handleLegacyErrors --- .../kibana-plugin-server.irouter.delete.md | 2 +- .../kibana-plugin-server.irouter.get.md | 2 +- ...lugin-server.irouter.handlelegacyerrors.md | 13 +++ .../server/kibana-plugin-server.irouter.md | 9 ++- .../kibana-plugin-server.irouter.post.md | 2 +- .../kibana-plugin-server.irouter.put.md | 2 +- .../core/server/kibana-plugin-server.md | 1 + .../kibana-plugin-server.routeregistrar.md | 13 +++ src/core/MIGRATION_EXAMPLES.md | 29 +++++++ src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/index.ts | 1 + .../http/integration_tests/router.test.ts | 47 +++++++++++ .../server/http/router/error_wrapper.test.ts | 80 +++++++++++++++++++ src/core/server/http/router/error_wrapper.ts | 48 +++++++++++ src/core/server/http/router/index.ts | 2 +- src/core/server/http/router/router.ts | 42 ++++++---- src/core/server/index.ts | 1 + src/core/server/server.api.md | 12 ++- 18 files changed, 279 insertions(+), 28 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeregistrar.md create mode 100644 src/core/server/http/router/error_wrapper.test.ts create mode 100644 src/core/server/http/router/error_wrapper.ts diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md index 9124b4a1b21c4..5202e0cfd5ebb 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -9,5 +9,5 @@ Register a route handler for `DELETE` request. Signature: ```typescript -delete:

(route: RouteConfig, handler: RequestHandler) => void; +delete: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md index 0291906c6fc6b..32552a49cb999 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.get.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -9,5 +9,5 @@ Register a route handler for `GET` request. Signature: ```typescript -get:

(route: RouteConfig, handler: RequestHandler) => void; +get: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md new file mode 100644 index 0000000000000..2367420068064 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) + +## IRouter.handleLegacyErrors property + +Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + +Signature: + +```typescript +handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index bbffe1e42f229..b5d3c893d745d 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -16,9 +16,10 @@ export interface IRouter | Property | Type | Description | | --- | --- | --- | -| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | -| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | -| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | -| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | +| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar | Register a route handler for GET request. | +| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar | Register a route handler for PUT request. | | [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md index e97a32e433ce9..cd655c9ce0dc8 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.post.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -9,5 +9,5 @@ Register a route handler for `POST` request. Signature: ```typescript -post:

(route: RouteConfig, handler: RequestHandler) => void; +post: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md index 25db91e389939..e553d4b79dd2b 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.put.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -9,5 +9,5 @@ Register a route handler for `PUT` request. Signature: ```typescript -put:

(route: RouteConfig, handler: RequestHandler) => void; +put: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index c6ab8502acbd2..360675b3490c2 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -170,6 +170,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | +| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md new file mode 100644 index 0000000000000..535927dc73743 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) + +## RouteRegistrar type + +Handler to declare a route. + +Signature: + +```typescript +export declare type RouteRegistrar =

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 9eed3a59acaa6..ccf14879baa37 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -298,6 +298,35 @@ class Plugin { } } ``` +If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` +as a temporary solution until error migration is complete: +```ts +// legacy/plugins/demoplugin/server/plugin.ts +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'src/core/server'; + +export interface DemoPluginsSetup {}; + +class Plugin { + public setup(core: CoreSetup, pluginSetup: DemoPluginSetup) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/demoplugin/search', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + router.wrapErrors((context, req, res) => { + throw Boom.notFound('not there'); // will be converted into proper New Platform error + }) + ) + } +} +``` + #### 4. New Platform plugin As the final step we delete the shim and move all our code into a New Platform diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 00c9aedc42cfb..e9a2571382edc 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,6 +45,7 @@ const createRouterMock = (): jest.Mocked => ({ put: jest.fn(), delete: jest.fn(), getRoutes: jest.fn(), + handleLegacyErrors: jest.fn().mockImplementation(handler => handler), }); const createSetupContractMock = () => { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index ff1ff3acfae3d..2fa67750f6406 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -45,6 +45,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 70d7ae00f917e..481d8e1bbf49b 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -164,6 +164,53 @@ describe('Handler', () => { }); }); +describe('handleLegacyErrors', () => { + it('properly convert Boom errors', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false }, + router.handleLegacyErrors((context, req, res) => { + throw Boom.notFound(); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('Not Found'); + }); + + it('returns default error when non-Boom errors are thrown', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { + path: '/', + validate: false, + }, + router.handleLegacyErrors((context, req, res) => { + throw new Error('Unexpected'); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, + }); + }); +}); + describe('Response factory', () => { describe('Success', () => { it('supports answering with json object', async () => { diff --git a/src/core/server/http/router/error_wrapper.test.ts b/src/core/server/http/router/error_wrapper.test.ts new file mode 100644 index 0000000000000..aa20b49dc9c91 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.test.ts @@ -0,0 +1,80 @@ +/* + * 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 Boom from 'boom'; +import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response'; +import { wrapErrors } from './error_wrapper'; +import { KibanaRequest, RequestHandler, RequestHandlerContext } from 'kibana/server'; + +const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); +}; + +describe('wrapErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = wrapErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBeInstanceOf(KibanaResponse); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = wrapErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); +}); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts new file mode 100644 index 0000000000000..706a9fe3b8887 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.ts @@ -0,0 +1,48 @@ +/* + * 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 Boom from 'boom'; +import { ObjectType, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest } from './request'; +import { KibanaResponseFactory } from './response'; +import { RequestHandler } from './router'; +import { RequestHandlerContext } from '../../../server'; + +export const wrapErrors =

( + handler: RequestHandler +): RequestHandler => { + return async ( + context: RequestHandlerContext, + request: KibanaRequest, TypeOf, TypeOf>, + response: KibanaResponseFactory + ) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e)) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 56ed9ca11edc1..f07ad3cfe85c0 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler, IRouter } from './router'; +export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestRoute, diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 6b7e2e3ad14cd..a13eae51a19a6 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -27,6 +27,7 @@ import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from '. import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; +import { wrapErrors } from './error_wrapper'; interface RouterRoute { method: RouteMethod; @@ -35,6 +36,15 @@ interface RouterRoute { handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>; } +/** + * Handler to declare a route. + * @public + */ +export type RouteRegistrar =

( + route: RouteConfig, + handler: RequestHandler +) => void; + /** * Registers route handlers for specified resource path and method. * See {@link RouteConfig} and {@link RequestHandler} for more information about arguments to route registrations. @@ -52,40 +62,36 @@ export interface IRouter { * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - get:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + get: RouteRegistrar; /** * Register a route handler for `POST` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - post:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + post: RouteRegistrar; /** * Register a route handler for `PUT` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - put:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + put: RouteRegistrar; /** * Register a route handler for `DELETE` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - delete:

( - route: RouteConfig, + delete: RouteRegistrar; + + /** + * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + * @param handler {@link RequestHandler} - a route handler to wrap + */ + handleLegacyErrors:

( handler: RequestHandler - ) => void; + ) => RequestHandler; /** * Returns all routes registered with the this router. @@ -188,6 +194,12 @@ export class Router implements IRouter { return [...this.routes]; } + public handleLegacyErrors

( + handler: RequestHandler + ): RequestHandler { + return wrapErrors(handler); + } + private async handle

({ routeSchemas, request, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 987e4e64f9d5b..31dec2c9b96ff 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -114,6 +114,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, SessionStorage, SessionStorageCookieOptions, SessionStorageFactory, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 066f79bfd38f3..d6cfa54397565 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -714,14 +714,15 @@ export interface IndexSettingsDeprecationInfo { // @public export interface IRouter { - delete:

(route: RouteConfig, handler: RequestHandler) => void; - get:

(route: RouteConfig, handler: RequestHandler) => void; + delete: RouteRegistrar; + get: RouteRegistrar; // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts // // @internal getRoutes: () => RouterRoute[]; - post:

(route: RouteConfig, handler: RequestHandler) => void; - put:

(route: RouteConfig, handler: RequestHandler) => void; + handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; + post: RouteRegistrar; + put: RouteRegistrar; routerPath: string; } @@ -1099,6 +1100,9 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +// @public +export type RouteRegistrar =

(route: RouteConfig, handler: RequestHandler) => void; + // @public (undocumented) export interface SavedObject { attributes: T; From cfed9c6c485f4812f7a2ebccbce09ce60a9c2d93 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 26 Nov 2019 10:35:56 +0000 Subject: [PATCH 33/52] [Task Manager] Tests for the ability to run tasks of varying durations in parallel (#51572) This PR adds a test that ensures Task Manager is capable of picking up new tasks in parallel to a long running tasks that might otherwise hold up task execution. This doesn't add functionality - just a missing test case. --- .../plugins/task_manager/index.js | 17 ++++- .../plugins/task_manager/init_routes.js | 71 +++++++++++++++---- .../task_manager/task_manager_integration.js | 53 +++++++++++++- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js index 938324c12a377..73253224bb45d 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +const { EventEmitter } = require('events'); + import { initRoutes } from './init_routes'; + +const once = function (emitter, event) { + return new Promise(resolve => { + emitter.once(event, resolve); + }); +}; + export default function TaskTestingAPI(kibana) { + const taskTestingEvents = new EventEmitter(); + return new kibana.Plugin({ name: 'sampleTask', require: ['elasticsearch', 'task_manager'], @@ -52,6 +63,10 @@ export default function TaskTestingAPI(kibana) { refresh: true, }); + if (params.waitForEvent) { + await once(taskTestingEvents, params.waitForEvent); + } + return { state: { count: (prevState.count || 0) + 1 }, runAt: millisecondsFromNow(params.nextRunMilliseconds), @@ -88,7 +103,7 @@ export default function TaskTestingAPI(kibana) { }, }); - initRoutes(server); + initRoutes(server, taskTestingEvents); }, }); } diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index a9dfabae6d609..7b9e265a15d6f 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -23,11 +23,44 @@ const taskManagerQuery = { } }; -export function initRoutes(server) { +export function initRoutes(server, taskTestingEvents) { const taskManager = server.plugins.task_manager; server.route({ - path: '/api/sample_tasks', + path: '/api/sample_tasks/schedule', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + task: Joi.object({ + taskType: Joi.string().required(), + interval: Joi.string().optional(), + params: Joi.object().required(), + state: Joi.object().optional(), + id: Joi.string().optional() + }) + }), + }, + }, + async handler(request) { + try { + const { task: taskFields } = request.payload; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await (taskManager.schedule(task, { request })); + + return taskResult; + } catch (err) { + return err; + } + }, + }); + + server.route({ + path: '/api/sample_tasks/ensure_scheduled', method: 'POST', config: { validate: { @@ -38,26 +71,19 @@ export function initRoutes(server) { params: Joi.object().required(), state: Joi.object().optional(), id: Joi.string().optional() - }), - ensureScheduled: Joi.boolean() - .default(false) - .optional(), + }) }), }, }, async handler(request) { try { - const { ensureScheduled = false, task: taskFields } = request.payload; + const { task: taskFields } = request.payload; const task = { ...taskFields, scope: [scope], }; - const taskResult = await ( - ensureScheduled - ? taskManager.ensureScheduled(task, { request }) - : taskManager.schedule(task, { request }) - ); + const taskResult = await (taskManager.ensureScheduled(task, { request })); return taskResult; } catch (err) { @@ -66,6 +92,27 @@ export function initRoutes(server) { }, }); + server.route({ + path: '/api/sample_tasks/event', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + event: Joi.string().required() + }), + }, + }, + async handler(request) { + try { + const { event } = request.payload; + taskTestingEvents.emit(event); + return { event }; + } catch (err) { + return err; + } + }, + }); + server.route({ path: '/api/sample_tasks', method: 'GET', diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index 9b4297e995cbd..986648f795da6 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -58,7 +58,7 @@ export default function ({ getService }) { } function scheduleTask(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/schedule') .set('kbn-xsrf', 'xxx') .send({ task }) .expect(200) @@ -66,13 +66,20 @@ export default function ({ getService }) { } function scheduleTaskIfNotExists(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/ensure_scheduled') .set('kbn-xsrf', 'xxx') - .send({ task, ensureScheduled: true }) + .send({ task }) .expect(200) .then((response) => response.body); } + function releaseTasksWaitingForEventToComplete(event) { + return supertest.post('/api/sample_tasks/event') + .set('kbn-xsrf', 'xxx') + .send({ event }) + .expect(200); + } + it('should support middleware', async () => { const historyItem = _.random(1, 100); @@ -204,5 +211,45 @@ export default function ({ getService }) { expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.greaterThan(expectedDiff - buffer); expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.lessThan(expectedDiff + buffer); } + + it('should run tasks in parallel, allowing for long running tasks along side faster tasks', async () => { + /** + * It's worth noting this test relies on the /event endpoint that forces Task Manager to hold off + * on completing a task until a call is made by the test suite. + * If we begin testing with multiple Kibana instacnes in Parallel this will likely become flaky. + * If you end up here because the test is flaky, this might be why. + */ + const fastTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { }, + }); + + const longRunningTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { + waitForEvent: 'rescheduleHasHappened' + }, + }); + + function getTaskById(tasks, id) { + return tasks.filter(task => task.id === id)[0]; + } + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + expect(getTaskById(tasks, fastTask.id).state.count).to.eql(2); + }); + + await releaseTasksWaitingForEventToComplete('rescheduleHasHappened'); + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + + expect(getTaskById(tasks, fastTask.id).state.count).to.greaterThan(2); + expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); + }); + }); }); } From 9e168391affcf75c0762fccf1c90bbc009413cf8 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 26 Nov 2019 13:45:37 +0200 Subject: [PATCH 34/52] =?UTF-8?q?Move=20apply=20filters=20popover=20?= =?UTF-8?q?=E2=87=92=20NP=20(#51566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move ApplyFiltersPopoverContent and applyFiltersPopover to NP * code review --- src/core/MIGRATION.md | 2 +- .../filter/action/apply_filter_action.ts | 2 +- .../apply_filters/apply_filters_popover.tsx | 74 ------------------- src/legacy/core_plugins/data/public/index.ts | 1 - .../apply_filter_popover_content.tsx | 11 +-- .../apply_filters/apply_filters_popover.tsx} | 23 +++++- .../data/public/ui}/apply_filters/index.ts | 2 +- src/plugins/data/public/ui/index.ts | 3 +- 8 files changed, 30 insertions(+), 88 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/apply_filters/apply_filter_popover_content.tsx (94%) rename src/{legacy/core_plugins/data/public/filter/index.tsx => plugins/data/public/ui/apply_filters/apply_filters_popover.tsx} (58%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/apply_filters/index.ts (92%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 22c96110742e0..c5e04c3cfb53a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1167,7 +1167,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/apply_filters'` | `import { applyFiltersPopover } from '../data/public'` | Directive is deprecated. | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | diff --git a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts index 39ec1f78b65f0..946b3997a9712 100644 --- a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts +++ b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts @@ -29,10 +29,10 @@ import { esFilters, FilterManager, TimefilterContract, + applyFiltersPopover, changeTimeFilter, extractTimeFilter, } from '../../../../../../plugins/data/public'; -import { applyFiltersPopover } from '../apply_filters/apply_filters_popover'; import { IndexPatternsStart } from '../../index_patterns'; export const GLOBAL_APPLY_FILTER_ACTION = 'GLOBAL_APPLY_FILTER_ACTION'; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx deleted file mode 100644 index 41f757e726c40..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx +++ /dev/null @@ -1,74 +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 { EuiModal, EuiOverlayMask } from '@elastic/eui'; -import React, { Component } from 'react'; -import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; -import { IndexPattern } from '../../index_patterns/index_patterns'; -import { esFilters } from '../../../../../../plugins/data/public'; - -interface Props { - filters: esFilters.Filter[]; - onCancel: () => void; - onSubmit: (filters: esFilters.Filter[]) => void; - indexPatterns: IndexPattern[]; -} - -interface State { - isFilterSelected: boolean[]; -} - -export class ApplyFiltersPopover extends Component { - public render() { - if (!this.props.filters || this.props.filters.length === 0) { - return ''; - } - - return ( - - - - - - ); - } -} - -type cancelFunction = () => void; -type submitFunction = (filters: esFilters.Filter[]) => void; -export const applyFiltersPopover = ( - filters: esFilters.Filter[], - indexPatterns: IndexPattern[], - onCancel: cancelFunction, - onSubmit: submitFunction -) => { - return ( - - ); -}; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index b33aef75e6756..c1b4226e6e49f 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,6 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { ApplyFiltersPopover } from './filter'; export { Field, FieldType, diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx similarity index 94% rename from src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx rename to src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index 954cbca8f054b..affbb8acecb20 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -30,17 +30,12 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { IndexPattern } from '../../index_patterns'; -import { - mapAndFlattenFilters, - esFilters, - utils, - FilterLabel, -} from '../../../../../../plugins/data/public'; +import { mapAndFlattenFilters, esFilters, utils, IIndexPattern } from '../..'; +import { FilterLabel } from '../filter_bar'; interface Props { filters: esFilters.Filter[]; - indexPatterns: IndexPattern[]; + indexPatterns: IIndexPattern[]; onCancel: () => void; onSubmit: (filters: esFilters.Filter[]) => void; } diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx similarity index 58% rename from src/legacy/core_plugins/data/public/filter/index.tsx rename to src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx index e48a18fc53a76..71a042adffa39 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -17,4 +17,25 @@ * under the License. */ -export { ApplyFiltersPopover } from './apply_filters'; +import React from 'react'; +import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; +import { IIndexPattern, esFilters } from '../..'; + +type CancelFnType = () => void; +type SubmitFnType = (filters: esFilters.Filter[]) => void; + +export const applyFiltersPopover = ( + filters: esFilters.Filter[], + indexPatterns: IIndexPattern[], + onCancel: CancelFnType, + onSubmit: SubmitFnType +) => { + return ( + + ); +}; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts b/src/plugins/data/public/ui/apply_filters/index.ts similarity index 92% rename from src/legacy/core_plugins/data/public/filter/apply_filters/index.ts rename to src/plugins/data/public/ui/apply_filters/index.ts index 6b64230ed6a0c..93c1245e1ffb0 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts +++ b/src/plugins/data/public/ui/apply_filters/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { ApplyFiltersPopover } from './apply_filters_popover'; +export { applyFiltersPopover } from './apply_filters_popover'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index d0aaf2f6aac1c..79107d1ede676 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export * from './filter_bar'; +export { FilterBar } from './filter_bar'; +export { applyFiltersPopover } from './apply_filters'; From 5dc24c9ee5d9ce68e492974d0ab9e0f6b92ec7e5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Nov 2019 12:49:30 +0100 Subject: [PATCH 35/52] Define spec provider for ActiveMQ meatricbeat module (#51698) --- .../tutorial_resources/logos/activemq.svg | 31 ++++++++++ .../tutorials/activemq_metrics/index.js | 61 +++++++++++++++++++ .../kibana/server/tutorials/register.js | 2 + 3 files changed, 94 insertions(+) create mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg create mode 100644 src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg new file mode 100644 index 0000000000000..20694ba6e62c7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js new file mode 100644 index 0000000000000..b76a9ee7c4dbe --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; + +export function activemqMetricsSpecProvider(context) { + const moduleName = 'activemq'; + return { + id: 'activemqMetrics', + name: i18n.translate('kbn.server.tutorials.activemqMetrics.nameTitle', { + defaultMessage: 'ActiveMQ metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', + }), + longDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.longDescription', { + defaultMessage: 'The `activemq` Metricbeat module fetches monitoring metrics from ActiveMQ instances \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/activemq.svg', + isBeta: true, + artifacts: { + application: { + label: i18n.translate('kbn.server.tutorials.corednsMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/kibana#/discover' + }, + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-activemq.html' + } + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 2d1aaa92b1e26..f36909e59f39b 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -80,6 +80,7 @@ import { consulMetricsSpecProvider } from './consul_metrics'; import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; import { traefikMetricsSpecProvider } from './traefik_metrics'; import { awsLogsSpecProvider } from './aws_logs'; +import { activemqMetricsSpecProvider } from './activemq_metrics'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -146,4 +147,5 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(cockroachdbMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(traefikMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(awsLogsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider); } From 0557a40a9db15def5ab1d55415e57114610ad111 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 26 Nov 2019 12:56:31 +0100 Subject: [PATCH 36/52] Document @kbn/config-schema. (#50307) --- .github/CODEOWNERS | 1 + packages/kbn-config-schema/README.md | 511 +++++++++++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 packages/kbn-config-schema/README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e2abd5a3db1c..e208dc73c7b4b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -61,6 +61,7 @@ /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform +/packages/kbn-config-schema/ @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md new file mode 100644 index 0000000000000..8ba2c43b5e1fe --- /dev/null +++ b/packages/kbn-config-schema/README.md @@ -0,0 +1,511 @@ +# `@kbn/config-schema` — The Kibana config validation library + +`@kbn/config-schema` is a TypeScript library inspired by Joi and designed to allow run-time validation of the +Kibana configuration entries providing developers with a fully typed model of the validated data. + +## Table of Contents + +- [Why `@kbn/config-schema`?](#why-kbnconfig-schema) +- [Schema building blocks](#schema-building-blocks) + - [Basic types](#basic-types) + - [`schema.string()`](#schemastring) + - [`schema.number()`](#schemanumber) + - [`schema.boolean()`](#schemaboolean) + - [`schema.literal()`](#schemaliteral) + - [Composite types](#composite-types) + - [`schema.arrayOf()`](#schemaarrayof) + - [`schema.object()`](#schemaobject) + - [`schema.recordOf()`](#schemarecordof) + - [`schema.mapOf()`](#schemamapof) + - [Advanced types](#advanced-types) + - [`schema.oneOf()`](#schemaoneof) + - [`schema.any()`](#schemaany) + - [`schema.maybe()`](#schemamaybe) + - [`schema.nullable()`](#schemanullable) + - [`schema.never()`](#schemanever) + - [`schema.uri()`](#schemauri) + - [`schema.byteSize()`](#schemabytesize) + - [`schema.duration()`](#schemaduration) + - [`schema.conditional()`](#schemaconditional) + - [References](#references) + - [`schema.contextRef()`](#schemacontextref) + - [`schema.siblingRef()`](#schemasiblingref) +- [Custom validation](#custom-validation) +- [Default values](#default-values) + +## Why `@kbn/config-schema`? + +Validation of externally supplied data is very important for Kibana. Especially if this data is used to configure how it operates. + +There are a number of reasons why we decided to roll our own solution for the configuration validation: + +* **Limited API surface** - having a future rich library is awesome, but it's a really hard task to audit such library and make sure everything is sane and secure enough. As everyone knows complexity is the enemy of security and hence we'd like to have a full control over what exactly we expose and commit to maintain. +* **Custom error messages** - detailed validation error messages are a great help to developers, but at the same time they can contain information that's way too sensitive to expose to everyone. We'd like to control these messages and make them only as detailed as really needed. For example, we don't want validation error messages to contain the passwords for internal users to show-up in the logs. These logs are commonly ingested into Elasticsearch, and accessible to a large number of users which shouldn't have access to the internal user's password. +* **Type information** - having run-time guarantees is great, but additionally having compile-time guarantees is even better. We'd like to provide developers with a fully typed model of the validated data so that it's harder to misuse it _after_ validation. +* **Upgradability** - no matter how well a validation library is implemented, it will have bugs and may need to be improved at some point anyway. Some external libraries are very well supported, some aren't or won't be in the future. It's always a risk to depend on an external party with their own release cadence when you need to quickly fix a security vulnerability in a patch version. We'd like to have a better control over lifecycle of such an important piece of our codebase. + +## Schema building blocks + +The schema is composed of one or more primitives depending on the shape of the data you'd like to validate. + +```typescript +const simpleStringSchema = schema.string(); +const moreComplexObjectSchema = schema.object({ name: schema.string() }); +``` + +Every schema instance has a `validate` method that is used to perform a validation of the data according to the schema. This method accepts three arguments: + +* `data: any` - **required**, data to be validated with the schema +* `context: Record` - **optional**, object whose properties can be referenced by the [context references](#schemacontextref) +* `namespace: string` - **optional**, arbitrary string that is used to prefix every error message thrown during validation + +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean(), + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); + +expect(valueSchema.validate({ isEnabled: true, env: 'prod' })).toEqual({ + isEnabled: true, + env: 'prod', +}); + +// Use default value for `env` from context via reference +expect(valueSchema.validate({ isEnabled: true }, { envName: 'staging' })).toEqual({ + isEnabled: true, + env: 'staging', +}); + +// Fail because of type mismatch +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }) +).toThrowError( + '[isEnabled]: expected value of type [boolean] but got [string]' +); + +// Fail because of type mismatch and prefix error with a custom namespace +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }, 'configuration') +).toThrowError( + '[configuration.isEnabled]: expected value of type [boolean] but got [string]' +); +``` + +__Notes:__ +* `validate` method throws as soon as the first schema violation is encountered, no further validation is performed. +* when you retrieve configuration within a Kibana plugin `validate` function is called by the Core automatically providing appropriate namespace and context variables (environment name, package info etc.). + +### Basic types + +#### `schema.string()` + +Validates input data as a string. + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minLength: number` - defines a minimum length the string should have. + * `maxLength: number` - defines a maximum length the string should have. + * `hostname: boolean` - indicates whether the string should be validated as a valid hostname (per [RFC 1123](https://tools.ietf.org/html/rfc1123)). + +__Usage:__ +```typescript +const valueSchema = schema.string({ maxLength: 10 }); +``` + +__Notes:__ +* By default `schema.string()` allows empty strings, to prevent that use non-zero value for `minLength` option. +* To validate a string using a regular expression use a custom validator function, see [Custom validation](#custom-validation) section for more details. + +#### `schema.number()` + +Validates input data as a number. + +__Output type:__ `number` + +__Options:__ + * `defaultValue: number | Reference | (() => number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: number` - defines a minimum value the number should have. + * `max: number` - defines a maximum value the number should have. + +__Usage:__ +```typescript +const valueSchema = schema.number({ max: 10 }); +``` + +__Notes:__ +* The `schema.number()` also supports a string as input if it can be safely coerced into number. + +#### `schema.boolean()` + +Validates input data as a boolean. + +__Output type:__ `boolean` + +__Options:__ + * `defaultValue: boolean | Reference | (() => boolean)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: boolean) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.boolean({ defaultValue: false }); +``` + +#### `schema.literal()` + +Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal. + +__Output type:__ `string`, `number` or `boolean` literals + +__Options:__ + * `defaultValue: TLiteral | Reference | (() => TLiteral)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TLiteral) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = [ + schema.literal('stringLiteral'), + schema.literal(100500), + schema.literal(false), +]; +``` + +### Composite types + +#### `schema.arrayOf()` + +Validates input data as a homogeneous array with the values being validated against predefined schema. + +__Output type:__ `TValue[]` + +__Options:__ + * `defaultValue: TValue[] | Reference | (() => TValue[])` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minSize: number` - defines a minimum size the array should have. + * `maxSize: number` - defines a maximum size the array should have. + +__Usage:__ +```typescript +const valueSchema = schema.arrayOf(schema.number()); +``` + +#### `schema.object()` + +Validates input data as an object with a predefined set of properties. + +__Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` + +__Options:__ + * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean({ defaultValue: false }), + name: schema.string({ minLength: 10 }), +}); +``` + +__Notes:__ +* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. + +#### `schema.recordOf()` + +Validates input data as an object with the keys and values being validated against predefined schema. + +__Output type:__ `Record` + +__Options:__ + * `defaultValue: Record | Reference> | (() => Record)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Record) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.recordOf(schema.string(), schema.number()); +``` + +__Notes:__ +* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. + +#### `schema.mapOf()` + +Validates input data as a map with the keys and values being validated against the predefined schema. + +__Output type:__ `Map` + +__Options:__ + * `defaultValue: Map | Reference> | (() => Map)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Map) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.mapOf(schema.string(), schema.number()); +``` + +### Advanced types + +#### `schema.oneOf()` + +Allows a list of alternative schemas to validate input data against. + +__Output type:__ `TValue1 | TValue2 | TValue3 | ..... as TUnion` + +__Options:__ + * `defaultValue: TUnion | Reference | (() => TUnion)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TUnion) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]); +``` + +__Notes:__ +* Since the result data type is a type union you should use various TypeScript type guards to get the exact type. + +#### `schema.any()` + +Indicates that input data shouldn't be validated and returned as is. + +__Output type:__ `any` + +__Options:__ + * `defaultValue: any | Reference | (() => any)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: any) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.any(); +``` + +__Notes:__ +* `schema.any()` is essentially an escape hatch for the case when your data can __really__ have any type and should be avoided at all costs. + +#### `schema.maybe()` + +Indicates that input data is optional and may not be present. + +__Output type:__ `T | undefined` + +__Usage:__ +```typescript +const valueSchema = schema.maybe(schema.string()); +``` + +__Notes:__ +* Don't use `schema.maybe()` if a nested type defines a default value. + +#### `schema.nullable()` + +Indicates that input data is optional and defaults to `null` if it's not present. + +__Output type:__ `T | null` + +__Usage:__ +```typescript +const valueSchema = schema.nullable(schema.string()); +``` + +__Notes:__ +* `schema.nullable()` also treats explicitly specified `null` as a valid input. + +#### `schema.never()` + +Indicates that input data is forbidden. + +__Output type:__ `never` + +__Usage:__ +```typescript +const valueSchema = schema.never(); +``` + +__Notes:__ +* `schema.never()` has a very limited application and usually used within [conditional schemas](#schemaconditional) to fully or partially forbid input data. + +#### `schema.uri()` + +Validates input data as a proper URI string (per [RFC 3986](https://tools.ietf.org/html/rfc3986)). + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `scheme: string | string[]` - limits allowed URI schemes to the one(s) defined here. + +__Usage:__ +```typescript +const valueSchema = schema.uri({ scheme: 'https' }); +``` + +__Notes:__ +* Prefer using `schema.uri()` for all URI validations even though it may be possible to replicate it with a custom validator for `schema.string()`. + +#### `schema.byteSize()` + +Validates input data as a proper digital data size. + +__Output type:__ `ByteSizeValue` + +__Options:__ + * `defaultValue: ByteSizeValue | string | number | Reference | (() => ByteSizeValue | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: ByteSizeValue | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: ByteSizeValue | string | number` - defines a minimum value the size should have. + * `max: ByteSizeValue | string | number` - defines a maximum value the size should have. + +__Usage:__ +```typescript +const valueSchema = schema.byteSize({ min: '3kb' }); +``` + +__Notes:__ +* The string value for `schema.byteSize()` and its options supports the following prefixes: `b`, `kb`, `mb`, `gb` and `tb`. +* The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`. +* Currently you cannot specify zero bytes with a string format and should use number `0` instead. + +#### `schema.duration()` + +Validates input data as a proper [duration](https://momentjs.com/docs/#/durations/). + +__Output type:__ `moment.Duration` + +__Options:__ + * `defaultValue: moment.Duration | string | number | Reference | (() => moment.Duration | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: moment.Duration | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.duration({ defaultValue: '70ms' }); +``` + +__Notes:__ +* The string value for `schema.duration()` supports the following prefixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. +* The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`. + +#### `schema.conditional()` + +Allows a specified condition that is evaluated _at the validation time_ and results in either one or another input validation schema. + +The first argument is always a [reference](#references) while the second one can be: +* another reference, in this cases both references are "dereferenced" and compared +* schema, in this case the schema is used to validate "dereferenced" value of the first reference +* value, in this case "dereferenced" value of the first reference is compared to that value + +The third argument is a schema that should be used if the result of the aforementioned comparison evaluates to `true`, otherwise `schema.conditional()` should fallback +to the schema provided as the fourth argument. + +__Output type:__ `TTrueResult | TFalseResult` + +__Options:__ + * `defaultValue: TTrueResult | TFalseResult | Reference | (() => TTrueResult | TFalseResult` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TTrueResult | TFalseResult) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + key: schema.oneOf([schema.literal('number'), schema.literal('string')]), + value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()), +}); +``` + +__Notes:__ +* Conditional schemas may be hard to read and understand and hence should be used only sparingly. + +### References + +#### `schema.contextRef()` + +Defines a reference to the value specified through the validation context. Context reference is only used as part of a [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); +valueSchema.validate({}, { envName: 'dev' }); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. +* The root context that Kibana provides during config validation includes lots of useful properties like `environment name` that can be used to provide a strict schema for production and more relaxed one for development. + +#### `schema.siblingRef()` + +Defines a reference to the value of the sibling key. Sibling references are only used a part of [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + node: schema.object({ tag: schema.string() }), + env: schema.string({ defaultValue: schema.siblingRef('node.tag') }), +}); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. + +## Custom validation + +Using built-in schema primitives may not be enough in some scenarios or sometimes the attempt to model complex schemas with built-in primitives only may result in unreadable code. +For these cases `@kbn/config-schema` provides a way to specify a custom validation function for almost any schema building block through the `validate` option. + +For example `@kbn/config-schema` doesn't have a dedicated primitive for the `RegExp` based validation currently, but you can easily do that with a custom `validate` function: + +```typescript +const valueSchema = schema.string({ + minLength: 3, + validate(value) { + if (!/^[a-z0-9_-]+$/.test(value)) { + return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; + } + }, +}); + +// ...or if you use that construct a lot... + +const regexSchema = (regex: RegExp) => schema.string({ + validate: value => regex.test(value) ? undefined : `must match "${regex.toString()}"`, +}); +const valueSchema = regexSchema(/^[a-z0-9_-]+$/); +``` + +Custom validation function is run _only after_ all built-in validations passed. It should either return a `string` as an error message +to denote the failed validation or not return anything at all (`void`) otherwise. Please also note that `validate` function is synchronous. + +Another use case for custom validation functions is when the schema depends on some run-time data: + +```typescript +const gesSchema = randomRunTimeSeed => schema.string({ + validate: value => value !== randomRunTimeSeed ? 'value is not allowed' : undefined +}); + +const schema = gesSchema('some-random-run-time-data'); +``` + +## Default values + +If you have an optional config field that you can have a default value for you may want to consider using dedicated `defaultValue` option to not +deal with "defined or undefined"-like checks all over the place in your code. You have three options to provide a default value for almost any schema primitive: + +* plain value that's known at the compile time +* [reference](#references) to a value that will be "dereferenced" at the validation time +* function that is invoked at the validation time and returns a plain value + +```typescript +const valueSchemaWithPlainValueDefault = schema.string({ defaultValue: 'n/a' }); +const valueSchemaWithReferencedValueDefault = schema.string({ defaultValue: schema.contextRef('env') }); +const valueSchemaWithFunctionEvaluatedDefault = schema.string({ defaultValue: () => Math.random().toString() }); +``` + +__Notes:__ +* `@kbn/config-schema` neither validates nor coerces default value and developer is responsible for making sure that it has the appropriate type. From e8e517475ae54b0276070c5dc1b7c4b0ac439fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 26 Nov 2019 13:19:11 +0100 Subject: [PATCH 37/52] [Security] Add message to login page (#51557) * [Security] Add loginAssistanceMessage to login page * Fix tests * Fix login_page.test.tsx * Fix defaultValue * Render login assistance message independently of other messages and use EuiText instead of EuiCallOut * Use small text Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * Flip order of message around --- docs/settings/security-settings.asciidoc | 7 +- .../resources/bin/kibana-docker | 1 + x-pack/legacy/plugins/security/index.js | 3 + .../basic_login_form.test.tsx.snap | 14 ++++ .../basic_login_form.test.tsx | 3 + .../basic_login_form/basic_login_form.tsx | 15 ++++ .../__snapshots__/login_page.test.tsx.snap | 83 +++++++++++++++++++ .../components/login_page/login_page.test.tsx | 19 +++++ .../components/login_page/login_page.tsx | 1 + .../security/public/views/login/login.tsx | 4 +- x-pack/plugins/security/server/config.test.ts | 73 ++++++++-------- x-pack/plugins/security/server/config.ts | 1 + x-pack/plugins/security/server/plugin.test.ts | 1 + x-pack/plugins/security/server/plugin.ts | 1 + 14 files changed, 188 insertions(+), 38 deletions(-) diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index b852d38c05dc9..805d991a9a0f3 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -20,7 +20,7 @@ are enabled. Do not set this to `false`; it disables the login form, user and role management screens, and authorization using <>. To disable {security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. +{ref}/security-settings.html[{es} security settings]. `xpack.security.audit.enabled`:: Set to `true` to enable audit logging for security events. By default, it is set @@ -40,7 +40,7 @@ An arbitrary string of 32 characters or more that is used to encrypt credentials in a cookie. It is crucial that this key is not exposed to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. -In addition, high-availability deployments of {kib} will behave unexpectedly +In addition, high-availability deployments of {kib} will behave unexpectedly if this setting isn't the same for all instances of {kib}. `xpack.security.secureCookies`:: @@ -53,3 +53,6 @@ routing requests through a load balancer or proxy). Sets the session duration (in milliseconds). By default, sessions stay active until the browser is closed. When this is set to an explicit timeout, closing the browser still requires the user to log back in to {kib}. + +`xpack.security.loginAssistanceMessage`:: +Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 6609b905b81ec..497307fa4124b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,7 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom ) diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index c098e3e67a6d9..d147c2572ceeb 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -31,6 +31,7 @@ export const security = (kibana) => new kibana.Plugin({ encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ legacyFallback: Joi.object({ enabled: Joi.boolean().default(true) // deprecated @@ -147,7 +148,9 @@ export const security = (kibana) => new kibana.Plugin({ server.injectUiAppVars('login', () => { const { showLogin, allowLogin, layout = 'form' } = securityPlugin.__legacyCompat.license.getFeatures(); + const { loginAssistanceMessage } = securityPlugin.__legacyCompat.config; return { + loginAssistanceMessage, loginState: { showLogin, allowLogin, diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap index 3b3024024a9cf..a08c454e569e6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap @@ -2,6 +2,20 @@ exports[`BasicLoginForm renders as expected 1`] = ` + + +

{ loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ) ).toMatchSnapshot(); @@ -68,6 +69,7 @@ describe('BasicLoginForm', () => { next={''} infoMessage={'Hey this is an info message'} intl={null as any} + loginAssistanceMessage="" /> ); @@ -86,6 +88,7 @@ describe('BasicLoginForm', () => { loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index 9dbb556f5f5f4..acdc29842d4c6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -7,6 +7,8 @@ import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { EuiText } from '@elastic/eui'; import { LoginState } from '../../../../../common/login_state'; interface Props { @@ -16,6 +18,7 @@ interface Props { loginState: LoginState; next: string; intl: InjectedIntl; + loginAssistanceMessage: string; } interface State { @@ -38,6 +41,7 @@ class BasicLoginFormUI extends Component { public render() { return ( + {this.renderLoginAssistanceMessage()} {this.renderMessage()} @@ -102,6 +106,16 @@ class BasicLoginFormUI extends Component { ); } + private renderLoginAssistanceMessage = () => { + return ( + + + {this.props.loginAssistanceMessage} + + + ); + }; + private renderMessage = () => { if (this.state.message) { return ( @@ -132,6 +146,7 @@ class BasicLoginFormUI extends Component { ); } + return null; }; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap index fc33c6e0a82cc..17ba81988414a 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap @@ -160,6 +160,88 @@ exports[`LoginPage disabled form states renders as expected when an unknown logi
`; +exports[`LoginPage disabled form states renders as expected when loginAssistanceMessage is set 1`] = ` +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + +
+
+`; + exports[`LoginPage disabled form states renders as expected when secure cookies are required but not present 1`] = `
{ loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: true, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -61,6 +62,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -76,6 +78,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -91,6 +94,21 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', + }; + + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when loginAssistanceMessage is set', () => { + const props = { + http: createMockHttp(), + window: {}, + next: '', + loginState: createLoginState(), + isSecureConnection: false, + requiresSecureConnection: false, + loginAssistanceMessage: 'This is an *important* message', }; expect(shallow()).toMatchSnapshot(); @@ -106,6 +124,7 @@ describe('LoginPage', () => { loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index 82dd0e679a5ee..e7e56947ca58f 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -31,6 +31,7 @@ interface Props { loginState: LoginState; isSecureConnection: boolean; requiresSecureConnection: boolean; + loginAssistanceMessage: string; } export class LoginPage extends Component { diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index 8b452e4c4fdf5..d9daf2d1f4d0d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -39,7 +39,8 @@ interface AnyObject { $http: AnyObject, $window: AnyObject, secureCookies: boolean, - loginState: LoginState + loginState: LoginState, + loginAssistanceMessage: string ) => { const basePath = chrome.getBasePath(); const next = parseNext($window.location.href, basePath); @@ -59,6 +60,7 @@ interface AnyObject { loginState={loginState} isSecureConnection={isSecure} requiresSecureConnection={secureCookies} + loginAssistanceMessage={loginAssistanceMessage} next={next} /> , diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 943d582bf484a..569611516c880 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,45 +13,48 @@ import { createConfig$, ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "sessionTimeout": null, + } + `); expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "sessionTimeout": null, + } + `); expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "loginAssistanceMessage": "", + "secureCookies": false, + "sessionTimeout": null, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 6fe3fc73e458c..a257a25344393 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -26,6 +26,7 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type) = export const ConfigSchema = schema.object( { + loginAssistanceMessage: schema.string({ defaultValue: '' }), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index b0e2ae7176834..2ff0e915fc1b0 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -52,6 +52,7 @@ describe('Security Plugin', () => { ], }, "cookieName": "sid", + "loginAssistanceMessage": undefined, "secureCookies": true, "sessionTimeout": 1500, }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 4b3997fe74f1b..c8761050524a5 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -205,6 +205,7 @@ export class Plugin { // We should stop exposing this config as soon as only new platform plugin consumes it. The only // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { + loginAssistanceMessage: config.loginAssistanceMessage, sessionTimeout: config.sessionTimeout, secureCookies: config.secureCookies, cookieName: config.cookieName, From 80879368a1f4dcc253b44b9406785ee668ba4778 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 26 Nov 2019 14:22:34 +0100 Subject: [PATCH 38/52] Typescriptify and shim kbn_tp_run_pipeline test plugin (#50645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Typscriptify and shim kbn_tp_run_pipeline test plugin * fix imports to not re-export ‘legacy’ from root of plugin --- scripts/functional_tests.js | 2 +- src/plugins/expressions/public/index.ts | 11 +- src/plugins/expressions/public/render.ts | 10 +- tasks/config/run.js | 2 +- test/functional/services/browser.ts | 5 +- test/interpreter_functional/README.md | 10 +- .../{config.js => config.ts} | 17 ++- .../{index.js => index.ts} | 26 ++-- .../plugins/kbn_tp_run_pipeline/public/app.js | 76 ----------- .../public/components/main.js | 91 ------------- .../kbn_tp_run_pipeline/public/index.ts | 20 +++ .../kbn_tp_run_pipeline/public/legacy.ts | 41 ++++++ .../public/np_ready/app/app.tsx | 28 ++++ .../public/np_ready/app/components/main.tsx | 122 +++++++++++++++++ .../public/np_ready/index.ts | 28 ++++ .../public/np_ready/plugin.ts | 45 +++++++ .../public/np_ready/services.ts | 23 ++++ .../public/np_ready/types.ts | 37 +++++ .../run_pipeline/{basic.js => basic.ts} | 55 +++++--- .../run_pipeline/{helpers.js => helpers.ts} | 126 +++++++++++++----- .../run_pipeline/{index.js => index.ts} | 11 +- .../run_pipeline/{metric.js => metric.ts} | 44 ++++-- .../{tag_cloud.js => tag_cloud.ts} | 39 ++++-- 23 files changed, 593 insertions(+), 276 deletions(-) rename test/interpreter_functional/{config.js => config.ts} (76%) rename test/interpreter_functional/plugins/kbn_tp_run_pipeline/{index.js => index.ts} (68%) delete mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js delete mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts rename test/interpreter_functional/test_suites/run_pipeline/{basic.js => basic.ts} (69%) rename test/interpreter_functional/test_suites/run_pipeline/{helpers.js => helpers.ts} (55%) rename test/interpreter_functional/test_suites/run_pipeline/{index.js => index.ts} (82%) rename test/interpreter_functional/test_suites/run_pipeline/{metric.js => metric.ts} (64%) rename test/interpreter_functional/test_suites/run_pipeline/{tag_cloud.js => tag_cloud.ts} (61%) diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 9f4e678c6adf5..b65cd3835cc0a 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -22,6 +22,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/plugin_functional/config.js'), - require.resolve('../test/interpreter_functional/config.js'), + require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), ]); diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index a14aaae98fc34..6dc88fd23f29a 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -20,10 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ExpressionsPublicPlugin(initializerContext); -} - export { ExpressionsPublicPlugin as Plugin }; export * from './plugin'; @@ -31,3 +27,10 @@ export * from './types'; export * from '../common'; export { interpreterProvider, ExpressionInterpret } from './interpreter_provider'; export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { ExpressionDataHandler } from './execute'; + +export { RenderResult, ExpressionRenderHandler } from './render'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ExpressionsPublicPlugin(initializerContext); +} diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 364d5f587bb6f..3c7008806e779 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -30,15 +30,17 @@ interface RenderError { export type IExpressionRendererExtraHandlers = Record; +export type RenderResult = RenderId | RenderError; + export class ExpressionRenderHandler { - render$: Observable; + render$: Observable; update$: Observable; events$: Observable; private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; - private renderSubject: Rx.BehaviorSubject; + private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; @@ -49,11 +51,11 @@ export class ExpressionRenderHandler { this.eventsSubject = new Rx.Subject(); this.events$ = this.eventsSubject.asObservable().pipe(share()); - this.renderSubject = new Rx.BehaviorSubject(null as RenderId | RenderError | null); + this.renderSubject = new Rx.BehaviorSubject(null as RenderResult | null); this.render$ = this.renderSubject.asObservable().pipe( share(), filter(_ => _ !== null) - ) as Observable; + ) as Observable; this.updateSubject = new Rx.Subject(); this.update$ = this.updateSubject.asObservable().pipe(share()); diff --git a/tasks/config/run.js b/tasks/config/run.js index ea5a4b01dc8a5..e4071c8b7d0ab 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -254,7 +254,7 @@ module.exports = function (grunt) { cmd: NODE, args: [ 'scripts/functional_tests', - '--config', 'test/interpreter_functional/config.js', + '--config', 'test/interpreter_functional/config.ts', '--bail', '--debug', '--kibana-install-dir', KIBANA_INSTALL_DIR, diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index a8ce4270d4205..ab686f4d5ffec 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -470,7 +470,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ); } - public async executeAsync(fn: string | ((...args: any[]) => R), ...args: any[]): Promise { + public async executeAsync( + fn: string | ((...args: any[]) => Promise), + ...args: any[] + ): Promise { return await driver.executeAsyncScript( fn, ...cloneDeep(args, arg => { diff --git a/test/interpreter_functional/README.md b/test/interpreter_functional/README.md index 336bfe3405a01..73df0ce4c9f04 100644 --- a/test/interpreter_functional/README.md +++ b/test/interpreter_functional/README.md @@ -3,7 +3,7 @@ This folder contains interpreter functional tests. Add new test suites into the `test_suites` folder and reference them from the -`config.js` file. These test suites work the same as regular functional test. +`config.ts` file. These test suites work the same as regular functional test. ## Run the test @@ -11,17 +11,17 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/interpreter_functional/config.js +node scripts/functional_tests_server.js --config test/interpreter_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts ``` # Writing tests -Look into test_suites/run_pipeline/basic.js for examples +Look into test_suites/run_pipeline/basic.ts for examples to update baseline screenshots and snapshots run with: ``` -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js --updateBaselines +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts --updateBaselines ``` \ No newline at end of file diff --git a/test/interpreter_functional/config.js b/test/interpreter_functional/config.ts similarity index 76% rename from test/interpreter_functional/config.js rename to test/interpreter_functional/config.ts index e8700262e273a..0fe7df4d50715 100644 --- a/test/interpreter_functional/config.js +++ b/test/interpreter_functional/config.ts @@ -19,25 +19,26 @@ import path from 'path'; import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -export default async function ({ readConfigFile }) { +export default async function({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); - const plugins = allFiles.filter(file => fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory()); + const plugins = allFiles.filter(file => + fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory() + ); return { - testFiles: [ - require.resolve('./test_suites/run_pipeline'), - ], + testFiles: [require.resolve('./test_suites/run_pipeline')], services: functionalConfig.get('services'), pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), esTestCluster: functionalConfig.get('esTestCluster'), apps: functionalConfig.get('apps'), esArchiver: { - directory: path.resolve(__dirname, '../es_archives') + directory: path.resolve(__dirname, '../es_archives'), }, snapshots: { directory: path.resolve(__dirname, 'snapshots'), @@ -49,7 +50,9 @@ export default async function ({ readConfigFile }) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), - ...plugins.map(pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`), + ...plugins.map( + pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` + ), ], }, }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts similarity index 68% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts index 95d6a555ebcf0..1d5564ec06e4e 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts @@ -16,24 +16,34 @@ * specific language governing permissions and limitations * under the License. */ +import { Legacy } from 'kibana'; +import { + ArrayOrItem, + LegacyPluginApi, + LegacyPluginSpec, + LegacyPluginOptions, +} from 'src/legacy/plugin_discovery/types'; -export default function (kibana) { - return new kibana.Plugin({ +// eslint-disable-next-line import/no-default-export +export default function(kibana: LegacyPluginApi): ArrayOrItem { + const pluginSpec: Partial = { + id: 'kbn_tp_run_pipeline', uiExports: { app: { title: 'Run Pipeline', description: 'This is a sample plugin to test running pipeline expressions', - main: 'plugins/kbn_tp_run_pipeline/app', - } + main: 'plugins/kbn_tp_run_pipeline/legacy', + }, }, - init(server) { + init(server: Legacy.Server) { // The following lines copy over some configuration variables from Kibana // to this plugin. This will be needed when embedding visualizations, so that e.g. // region map is able to get its configuration. server.injectUiAppVars('kbn_tp_run_pipeline', async () => { - return await server.getInjectedUiAppVars('kibana'); + return server.getInjectedUiAppVars('kibana'); }); - } - }); + }, + }; + return new kibana.Plugin(pluginSpec); } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js deleted file mode 100644 index e9ab2a4169915..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js +++ /dev/null @@ -1,76 +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 from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; -import { registries } from 'plugins/interpreter/registries'; -import { npStart } from 'ui/new_platform'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -// These are all the required uiExports you need to import in case you want to embed visualizations. -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visEditorTypes'; -import 'uiExports/visualize'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/search'; -import 'uiExports/interpreter'; - -import { Main } from './components/main'; - -const app = uiModules.get('apps/kbnRunPipelinePlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(
, domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('kbnRunPipelinePlugin', RootController); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js deleted file mode 100644 index 3e19d3a4d78ec..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js +++ /dev/null @@ -1,91 +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 from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, -} from '@elastic/eui'; -import { first } from 'rxjs/operators'; - -class Main extends React.Component { - chartDiv = React.createRef(); - - constructor(props) { - super(props); - - this.state = { - expression: '', - }; - - window.runPipeline = async (expression, context = {}, initialContext = {}) => { - this.setState({ expression }); - const adapters = { - requests: new props.RequestAdapter(), - data: new props.DataAdapter(), - }; - return await props.expressions.execute(expression, { - inspectorAdapters: adapters, - context, - searchContext: initialContext, - }).getData(); - }; - - let lastRenderHandler; - window.renderPipelineResponse = async (context = {}) => { - if (lastRenderHandler) { - lastRenderHandler.destroy(); - } - - lastRenderHandler = props.expressions.render(this.chartDiv, context); - const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); - - if (typeof renderResult === 'object' && renderResult.type === 'error') { - return this.setState({ expression: 'Render error!\n\n' + JSON.stringify(renderResult.error) }); - } - }; - } - - - render() { - const pStyle = { - display: 'flex', - width: '100%', - height: '300px' - }; - - return ( - - - - - runPipeline tests are running ... - -
this.chartDiv = ref} style={pStyle}/> -
{this.state.expression}
- - - - ); - } -} - -export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts new file mode 100644 index 0000000000000..c4cc7175d6157 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './np_ready'; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts new file mode 100644 index 0000000000000..39ce2b3077c96 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts @@ -0,0 +1,41 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/public'; +import { npSetup, npStart } from 'ui/new_platform'; + +import { plugin } from './np_ready'; + +// This is required so some default styles and required scripts/Angular modules are loaded, +// or the timezone setting is correctly applied. +import 'ui/autoload/all'; +// Used to run esaggs queries +import 'uiExports/fieldFormats'; +import 'uiExports/search'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; +// Used for kibana_context function + +import 'uiExports/savedObjectTypes'; +import 'uiExports/interpreter'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx new file mode 100644 index 0000000000000..f47a7c3a256f0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { Main } from './components/main'; + +export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { + render(
, element); + return () => unmountComponentAtNode(element); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx new file mode 100644 index 0000000000000..c091765619a19 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; +import { first } from 'rxjs/operators'; +import { + RequestAdapter, + DataAdapter, +} from '../../../../../../../../src/plugins/inspector/public/adapters'; +import { + Adapters, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from '../../types'; +import { getExpressions } from '../../services'; + +declare global { + interface Window { + runPipeline: ( + expressions: string, + context?: Context, + initialContext?: Context + ) => ReturnType; + renderPipelineResponse: (context?: Context) => Promise; + } +} + +interface State { + expression: string; +} + +class Main extends React.Component<{}, State> { + chartRef = React.createRef(); + + constructor(props: {}) { + super(props); + + this.state = { + expression: '', + }; + + window.runPipeline = async ( + expression: string, + context: Context = {}, + initialContext: Context = {} + ) => { + this.setState({ expression }); + const adapters: Adapters = { + requests: new RequestAdapter(), + data: new DataAdapter(), + }; + return getExpressions() + .execute(expression, { + inspectorAdapters: adapters, + context, + // TODO: naming / typing is confusing and doesn't match here + // searchContext is also a way to set initialContext and Context can't be set to SearchContext + searchContext: initialContext as any, + }) + .getData(); + }; + + let lastRenderHandler: ExpressionRenderHandler; + window.renderPipelineResponse = async (context = {}) => { + if (lastRenderHandler) { + lastRenderHandler.destroy(); + } + + lastRenderHandler = getExpressions().render(this.chartRef.current!, context); + const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); + + if (typeof renderResult === 'object' && renderResult.type === 'error') { + this.setState({ + expression: 'Render error!\n\n' + JSON.stringify(renderResult.error), + }); + } + + return renderResult; + }; + } + + render() { + const pStyle = { + display: 'flex', + width: '100%', + height: '300px', + }; + + return ( + + + + runPipeline tests are running ... +
+
{this.state.expression}
+ + + + ); + } +} + +export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts new file mode 100644 index 0000000000000..d7a764b581c01 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { PluginInitializer, PluginInitializerContext } from 'src/core/public'; +import { Plugin, StartDeps } from './plugin'; +export { StartDeps }; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new Plugin(initializerContext); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..348ba215930b0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts @@ -0,0 +1,45 @@ +/* + * 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 { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { ExpressionsStart } from './types'; +import { setExpressions } from './services'; + +export interface StartDeps { + expressions: ExpressionsStart; +} + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup({ application }: CoreSetup) { + application.register({ + id: 'kbn_tp_run_pipeline', + title: 'Run Pipeline', + async mount(context, params) { + const { renderApp } = await import('./app/app'); + return renderApp(context, params); + }, + }); + } + + public start(start: CoreStart, { expressions }: StartDeps) { + setExpressions(expressions); + } +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts new file mode 100644 index 0000000000000..657d8d5150c3a --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts @@ -0,0 +1,23 @@ +/* + * 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 { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core'; +import { ExpressionsStart } from './types'; + +export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts new file mode 100644 index 0000000000000..082bb47d80066 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from 'src/plugins/expressions/public'; + +import { Adapters } from 'src/plugins/inspector/public'; + +export { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, + Adapters, +}; diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.js b/test/interpreter_functional/test_suites/run_pipeline/basic.ts similarity index 69% rename from test/interpreter_functional/test_suites/run_pipeline/basic.js rename to test/interpreter_functional/test_suites/run_pipeline/basic.ts index 893a79956093c..77853b0bcd6a4 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.js +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -18,13 +18,16 @@ */ import expect from '@kbn/expect'; -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline +// this file showcases how to use testing utilities defined in helpers.ts together with the kbn_tp_run_pipeline // test plugin to write autmated tests for interprete -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('basic visualize loader pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); @@ -39,7 +42,12 @@ export default function ({ getService, updateBaselines }) { }); it('correctly sets timeRange', async () => { - const result = await expectExpression('correctly_sets_timerange', 'kibana', {}, { timeRange: 'test' }).getResponse(); + const result = await expectExpression( + 'correctly_sets_timerange', + 'kibana', + {}, + { timeRange: 'test' } + ).getResponse(); expect(result).to.have.property('timeRange', 'test'); }); }); @@ -60,30 +68,32 @@ export default function ({ getService, updateBaselines }) { // we can also do snapshot comparison of result of our expression // to update the snapshots run the tests with --updateBaselines - it ('runs the expression and compares final output', async () => { + it('runs the expression and compares final output', async () => { await expectExpression('final_output_test', expression).toMatchSnapshot(); }); // its also possible to check snapshot at every step of expression (after execution of each function) - it ('runs the expression and compares output at every step', async () => { + it('runs the expression and compares output at every step', async () => { await expectExpression('step_output_test', expression).steps.toMatchSnapshot(); }); // and we can do screenshot comparison of the rendered output of expression (if expression returns renderable) - it ('runs the expression and compares screenshots', async () => { + it('runs the expression and compares screenshots', async () => { await expectExpression('final_screenshot_test', expression).toMatchScreenshot(); }); // it is also possible to combine different checks - it ('runs the expression and combines different checks', async () => { - await (await expectExpression('combined_test', expression).steps.toMatchSnapshot()).toMatchScreenshot(); + it('runs the expression and combines different checks', async () => { + await ( + await expectExpression('combined_test', expression).steps.toMatchSnapshot() + ).toMatchScreenshot(); }); }); // if we want to do multiple different tests using the same data, or reusing a part of expression its // possible to retrieve the intermediate result and reuse it in later expressions describe('reusing partial results', () => { - it ('does some screenshot comparisons', async () => { + it('does some screenshot comparisons', async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, {"id":"2","enabled":true,"type":"terms","schema":"segment","params": @@ -93,17 +103,20 @@ export default function ({ getService, updateBaselines }) { const context = await expectExpression('partial_test', expression).getResponse(); // we reuse that response to render 3 different charts and compare screenshots with baselines - const tagCloudExpr = - `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await (await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const tagCloudExpr = `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); - const metricExpr = - `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await (await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const metricExpr = `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); - const regionMapExpr = - `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; - await (await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + await ( + await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.js b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts similarity index 55% rename from test/interpreter_functional/test_suites/run_pipeline/helpers.js rename to test/interpreter_functional/test_suites/run_pipeline/helpers.ts index 4df86d3418f1f..e1ec18fae5e3a 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.js +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -18,14 +18,45 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { + ExpressionDataHandler, + RenderResult, + Context, +} from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; + +type UnWrapPromise = T extends Promise ? U : T; +export type ExpressionResult = UnWrapPromise>; + +export type ExpectExpression = ( + name: string, + expression: string, + context?: Context, + initialContext?: Context +) => ExpectExpressionHandler; + +export interface ExpectExpressionHandler { + toReturn: (expectedResult: ExpressionResult) => Promise; + getResponse: () => Promise; + runExpression: (step?: string, stepContext?: Context) => Promise; + steps: { + toMatchSnapshot: () => Promise; + }; + toMatchSnapshot: () => Promise; + toMatchScreenshot: () => Promise; +} // helper for testing interpreter expressions -export const expectExpressionProvider = ({ getService, updateBaselines }) => { +export function expectExpressionProvider({ + getService, + updateBaselines, +}: Pick & { updateBaselines: boolean }): ExpectExpression { const browser = getService('browser'); const screenshot = getService('screenshots'); const snapshots = getService('snapshots'); const log = getService('log'); const testSubjects = getService('testSubjects'); + /** * returns a handler object to test a given expression * @name: name of the test @@ -34,20 +65,25 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @initialContext: initialContext provided to the expression * @returns handler object */ - return (name, expression, context = {}, initialContext = {}) => { + return ( + name: string, + expression: string, + context: Context = {}, + initialContext: Context = {} + ): ExpectExpressionHandler => { log.debug(`executing expression ${expression}`); const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast - let responsePromise; + let responsePromise: Promise; - const handler = { + const handler: ExpectExpressionHandler = { /** * checks if provided object matches expression result * @param result: expected expression result * @returns {Promise} */ - toReturn: async result => { + toReturn: async (expectedResult: ExpressionResult) => { const pipelineResponse = await handler.getResponse(); - expect(pipelineResponse).to.eql(result); + expect(pipelineResponse).to.eql(expectedResult); }, /** * returns expression response @@ -63,16 +99,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @param stepContext: context to provide to expression * @returns {Promise<*>} result of running expression */ - runExpression: async (step, stepContext) => { + runExpression: async ( + step: string = expression, + stepContext: Context = context + ): Promise => { log.debug(`running expression ${step || expression}`); - const promise = browser.executeAsync((expression, context, initialContext, done) => { - if (!context) context = {}; - if (!context.type) context.type = 'null'; - window.runPipeline(expression, context, initialContext).then(result => { - done(result); - }); - }, step || expression, stepContext || context, initialContext); - return await promise; + return browser.executeAsync( + ( + _expression: string, + _currentContext: Context & { type: string }, + _initialContext: Context, + done: (expressionResult: ExpressionResult) => void + ) => { + if (!_currentContext) _currentContext = { type: 'null' }; + if (!_currentContext.type) _currentContext.type = 'null'; + return window + .runPipeline(_expression, _currentContext, _initialContext) + .then(expressionResult => { + done(expressionResult); + return expressionResult; + }); + }, + step, + stepContext, + initialContext + ); }, steps: { /** @@ -80,17 +131,19 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @returns {Promise} */ toMatchSnapshot: async () => { - let lastResponse; + let lastResponse: ExpressionResult; for (let i = 0; i < steps.length; i++) { const step = steps[i]; - lastResponse = await handler.runExpression(step, lastResponse); - const diff = await snapshots.compareAgainstBaseline(name + i, toSerializable(lastResponse), updateBaselines); + lastResponse = await handler.runExpression(step, lastResponse!); + const diff = await snapshots.compareAgainstBaseline( + name + i, + toSerializable(lastResponse!), + updateBaselines + ); expect(diff).to.be.lessThan(0.05); } if (!responsePromise) { - responsePromise = new Promise(resolve => { - resolve(lastResponse); - }); + responsePromise = Promise.resolve(lastResponse!); } return handler; }, @@ -101,7 +154,11 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { */ toMatchSnapshot: async () => { const pipelineResponse = await handler.getResponse(); - await snapshots.compareAgainstBaseline(name, toSerializable(pipelineResponse), updateBaselines); + await snapshots.compareAgainstBaseline( + name, + toSerializable(pipelineResponse), + updateBaselines + ); return handler; }, /** @@ -111,24 +168,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { toMatchScreenshot: async () => { const pipelineResponse = await handler.getResponse(); log.debug('starting to render'); - const result = await browser.executeAsync((context, done) => { - window.renderPipelineResponse(context).then(result => { - done(result); - }); - }, pipelineResponse); + const result = await browser.executeAsync( + (_context: ExpressionResult, done: (renderResult: RenderResult) => void) => + window.renderPipelineResponse(_context).then(renderResult => { + done(renderResult); + return renderResult; + }), + pipelineResponse + ); log.debug('response of rendering: ', result); const chartEl = await testSubjects.find('pluginChart'); - const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines, chartEl); + const percentDifference = await screenshot.compareAgainstBaseline( + name, + updateBaselines, + chartEl + ); expect(percentDifference).to.be.lessThan(0.1); return handler; - } + }, }; return handler; }; - function toSerializable(response) { + function toSerializable(response: ExpressionResult) { if (response.error) { // in case of error, pass through only message to the snapshot // as error could be expected and stack trace shouldn't be part of the snapshot @@ -136,4 +200,4 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { } return response; } -}; +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.js b/test/interpreter_functional/test_suites/run_pipeline/index.ts similarity index 82% rename from test/interpreter_functional/test_suites/run_pipeline/index.js rename to test/interpreter_functional/test_suites/run_pipeline/index.ts index 3c1ce2314f55f..031a0e3576ccc 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.js +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -17,7 +17,9 @@ * under the License. */ -export default function ({ getService, getPageObjects, loadTestFile }) { +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -25,13 +27,16 @@ export default function ({ getService, getPageObjects, loadTestFile }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'header']); - describe('runPipeline', function () { + describe('runPipeline', function() { this.tags(['skipFirefox']); before(async () => { await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional'); await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding'); - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', 'defaultIndex': 'logstash-*' }); + await kibanaServer.uiSettings.replace({ + 'dateFormat:tz': 'Australia/North', + defaultIndex: 'logstash-*', + }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); await appsMenu.clickLink('Run Pipeline'); diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.js b/test/interpreter_functional/test_suites/run_pipeline/metric.ts similarity index 64% rename from test/interpreter_functional/test_suites/run_pipeline/metric.js rename to test/interpreter_functional/test_suites/run_pipeline/metric.ts index 78d571b3583be..c238bedfa28ce 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.js +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -17,18 +17,21 @@ * under the License. */ -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('metricVis pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); describe('correctly renders metric', () => { - let dataContext; + let dataContext: ExpressionResult; before(async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, @@ -44,27 +47,46 @@ export default function ({ getService, updateBaselines }) { it('with invalid data', async () => { const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('metric_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with single metric data', async () => { const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression( + 'metric_single_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with multiple metric data', async () => { const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; - await (await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression( + 'metric_multi_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with metric and bucket data', async () => { const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; - await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with percentage option', async () => { - const expression = 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; - await (await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; + await ( + await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts similarity index 61% rename from test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js rename to test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts index 7c0e2d7190703..2451df4db6310 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js +++ b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts @@ -17,18 +17,21 @@ * under the License. */ -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('tag cloud pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); describe('correctly renders tagcloud', () => { - let dataContext; + let dataContext: ExpressionResult; before(async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, @@ -41,27 +44,39 @@ export default function ({ getService, updateBaselines }) { it('with invalid data', async () => { const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with just metric data', async () => { const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with metric and bucket data', async () => { const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1}'; - await (await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with font size options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; - await (await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; + await ( + await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with scale and orientation options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; - await (await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; + await ( + await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); From 5899aa5a8c409e88e03c3f67b0580cabc21265da Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 26 Nov 2019 14:23:10 +0100 Subject: [PATCH 39/52] Calculate Console app height (#51707) --- src/legacy/core_plugins/console/public/quarantined/_app.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 1e13b6b483981..b19fd438f8ee3 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,5 +1,8 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). +@import '@elastic/eui/src/components/header/variables'; + #consoleRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); display: flex; flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container From ac0e3e12e2fcdaafe74acfb27044471f3ffd4dce Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 26 Nov 2019 14:24:06 +0100 Subject: [PATCH 40/52] [SearchProfiler] Copy updates (#51700) * Update search profiler copy * Fix pristine logic * Update searchprofiler styles --- .../components/empty_tree_placeholder.tsx | 5 ++--- .../profile_loading_placeholder.tsx | 2 +- .../application/containers/main/main.tsx | 2 +- .../np_ready/application/store/reducer.ts | 7 +------ .../np_ready/application/store/store.ts | 2 +- .../np_ready/application/styles/_index.scss | 21 +++---------------- .../styles/components/_profile_tree.scss | 4 ---- 7 files changed, 9 insertions(+), 34 deletions(-) diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx index bf27620dcac18..d709a8feb48bd 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx @@ -15,13 +15,12 @@ export const EmptyTreePlaceHolder = () => { {/* TODO: translations */}

{i18n.translate('xpack.searchProfiler.emptyProfileTreeTitle', { - defaultMessage: 'Nothing to see here yet.', + defaultMessage: 'No queries to profile', })}

{i18n.translate('xpack.searchProfiler.emptyProfileTreeDescription', { - defaultMessage: - 'Enter a query and press the "Profile" button or provide profile data in the editor.', + defaultMessage: 'Enter a query, click Profile, and see the results here.', })}

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx index fb09c6cddf70a..a7db54b670a84 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx @@ -13,7 +13,7 @@ export const ProfileLoadingPlaceholder = () => {

{i18n.translate('xpack.searchProfiler.profilingLoaderText', { - defaultMessage: 'Profiling...', + defaultMessage: 'Loading query profiles...', })}

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx index 7f5d223949e61..63ae5c7583625 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx @@ -93,7 +93,7 @@ export const Main = () => { return ( <> - + {renderLicenseWarning()} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts index dac9dab9bd092..615511786afd1 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts @@ -12,7 +12,6 @@ import { OnHighlightChangeArgs } from '../components/profile_tree'; import { ShardSerialized, Targets } from '../types'; export type Action = - | { type: 'setPristine'; value: boolean } | { type: 'setProfiling'; value: boolean } | { type: 'setHighlightDetails'; value: OnHighlightChangeArgs | null } | { type: 'setActiveTab'; value: Targets | null } @@ -20,12 +19,8 @@ export type Action = export const reducer: Reducer = (state, action) => produce(state, draft => { - if (action.type === 'setPristine') { - draft.pristine = action.value; - return; - } - if (action.type === 'setProfiling') { + draft.pristine = false; draft.profiling = action.value; if (draft.profiling) { draft.currentResponse = null; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts index 7b5a1ce93583d..7008854a16285 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts @@ -18,7 +18,7 @@ export interface State { export const initialState: State = { profiling: false, - pristine: false, + pristine: true, highlightDetails: null, activeTab: null, currentResponse: null, diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss index a72d079354f89..d36a587b9257f 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss @@ -10,12 +10,6 @@ @import 'containers/main'; @import 'containers/profile_query_editor'; -#searchProfilerAppRoot { - height: 100%; - display: flex; - flex: 1 1 auto; -} - .prfDevTool__licenseWarning { &__container { max-width: 1000px; @@ -55,19 +49,10 @@ } } -.prfDevTool { - height: calc(100vh - #{$euiHeaderChildSize}); +.appRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); overflow: hidden; - - .devApp__container { - height: 100%; - overflow: hidden; - flex-shrink: 1; - } - - &__container { - overflow: hidden; - } + flex-shrink: 1; } .prfDevTool__detail { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss index cc4d334f58fd3..c7dc4a305acb2 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss @@ -5,10 +5,6 @@ $badgeSize: $euiSize * 5.5; .prfDevTool__profileTree { - &__container { - height: 100%; - } - &__shardDetails--dim small { color: $euiColorDarkShade; } From 07bc6907776a4c94c072b6934617ad6a2ee477a7 Mon Sep 17 00:00:00 2001 From: ffknob Date: Tue, 26 Nov 2019 10:29:56 -0300 Subject: [PATCH 41/52] [SR] Prevents negative values for Snapshot retention policies (#51295) --- .../client_integration/policy_add.test.ts | 21 +++++++++++++ .../policy_form/steps/step_retention.tsx | 6 +++- .../services/validation/validate_policy.ts | 30 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index 2d85a61b04852..bc48d6d6312fb 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -18,6 +18,8 @@ jest.mock('ui/i18n', () => { return { I18nContext }; }); +jest.mock('ui/new_platform'); + const POLICY_NAME = 'my_policy'; const SNAPSHOT_NAME = 'my_snapshot'; const MIN_COUNT = '5'; @@ -141,6 +143,25 @@ describe.skip('', () => { 'Minimum count cannot be greater than maximum count.', ]); }); + + test('should not allow negative values for the delete after, minimum and maximum counts', () => { + const { find, form } = testBed; + + form.setInputValue('expireAfterValueInput', '-1'); + find('expireAfterValueInput').simulate('blur'); + + form.setInputValue('minCountInput', '-1'); + find('minCountInput').simulate('blur'); + + form.setInputValue('maxCountInput', '-1'); + find('maxCountInput').simulate('blur'); + + expect(form.getErrorsMessages()).toEqual([ + 'Delete after cannot be negative.', + 'Minimum count cannot be negative.', + 'Maximum count cannot be negative.', + ]); + }); }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx index c88cbd2736df6..df7e2c8807d9f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx @@ -85,7 +85,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ } describedByIds={['expirationDescription']} isInvalid={touched.expireAfterValue && Boolean(errors.expireAfterValue)} - error={errors.expireAfter} + error={errors.expireAfterValue} fullWidth > @@ -100,6 +100,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="expireAfterValueInput" + min={0} /> @@ -167,6 +168,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="minCountInput" + min={0} /> @@ -179,6 +181,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ /> } describedByIds={['countDescription']} + isInvalid={touched.maxCount && Boolean(errors.maxCount)} error={errors.maxCount} fullWidth > @@ -193,6 +196,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="maxCountInput" + min={0} /> diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts index 80734d2f0522c..3f27da82bf56d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts @@ -28,7 +28,9 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { schedule: [], repository: [], indices: [], + expireAfterValue: [], minCount: [], + maxCount: [], }, }; @@ -92,6 +94,34 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { }) ); } + + if (retention && retention.expireAfterValue && retention.expireAfterValue < 0) { + validation.errors.expireAfterValue.push( + i18n.translate( + 'xpack.snapshotRestore.policyValidation.invalidNegativeDeleteAfterErrorMessage', + { + defaultMessage: 'Delete after cannot be negative.', + } + ) + ); + } + + if (retention && retention.minCount && retention.minCount < 0) { + validation.errors.minCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMinCountErrorMessage', { + defaultMessage: 'Minimum count cannot be negative.', + }) + ); + } + + if (retention && retention.maxCount && retention.maxCount < 0) { + validation.errors.maxCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMaxCountErrorMessage', { + defaultMessage: 'Maximum count cannot be negative.', + }) + ); + } + // Remove fields with no errors validation.errors = Object.entries(validation.errors) .filter(([key, value]) => value.length > 0) From 75d261d48cd1d777656f3b8ec46ba45019680b2c Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 26 Nov 2019 15:49:53 +0200 Subject: [PATCH 42/52] =?UTF-8?q?Move=20IndexPatternsSelector=20=E2=87=92?= =?UTF-8?q?=20NP=20(#51620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move IndexPatternsSelector to NP Replace import from ui/index_patterns with new platform imports * karma mock * added mock * Fix jest tests --- src/legacy/core_plugins/data/public/index.ts | 1 - .../index_patterns_service.mock.ts | 1 - .../index_patterns/index_patterns_service.ts | 5 +-- .../data/public/index_patterns/utils.ts | 13 -------- .../components/editor/controls_tab.test.js | 23 +++++++++++-- .../editor/index_pattern_select_form_row.js | 5 +-- .../editor/list_control_editor.test.js | 16 +++++++-- .../editor/range_control_editor.test.js | 21 +++++++++--- .../public/index_patterns/__mocks__/index.ts | 1 - src/legacy/ui/public/index_patterns/index.ts | 1 - .../new_platform/new_platform.karma_mock.js | 7 ++++ .../lib/get_index_pattern_title.ts | 33 +++++++++++++++++++ .../data/public/index_patterns/lib/index.ts | 20 +++++++++++ src/plugins/data/public/mocks.ts | 3 ++ src/plugins/data/public/plugin.ts | 4 +++ src/plugins/data/public/types.ts | 4 +++ src/plugins/data/public/ui/index.ts | 1 + .../public/ui/index_pattern_select}/index.ts | 0 .../index_pattern_select.tsx | 6 ++-- .../join_editor/resources/join_expression.js | 4 ++- .../create_source_editor.js | 4 ++- .../es_pew_pew_source/create_source_editor.js | 3 +- .../es_search_source/create_source_editor.js | 4 ++- 23 files changed, 141 insertions(+), 39 deletions(-) create mode 100644 src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts create mode 100644 src/plugins/data/public/index_patterns/lib/index.ts rename src/{legacy/core_plugins/data/public/index_patterns/components => plugins/data/public/ui/index_pattern_select}/index.ts (100%) rename src/{legacy/core_plugins/data/public/index_patterns/components => plugins/data/public/ui/index_pattern_select}/index_pattern_select.tsx (97%) diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index c1b4226e6e49f..1349187779061 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -47,7 +47,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts index 5dcf4005ef4e8..db1ece78e7b4d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts @@ -33,7 +33,6 @@ const createSetupContractMock = () => { flattenHitWrapper: jest.fn().mockImplementation(flattenHitWrapper), formatHitProvider: jest.fn(), indexPatterns: jest.fn() as any, - IndexPatternSelect: jest.fn(), __LEGACY: { // For BWC we must temporarily export the class implementation of Field, // which is only used externally by the Index Pattern UI. diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index f97246bc5a9bf..381cd491f0210 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -25,7 +25,6 @@ import { } from 'src/core/public'; import { FieldFormatsStart } from '../../../../../plugins/data/public'; import { Field, FieldList, FieldListInterface, FieldType } from './fields'; -import { createIndexPatternSelect } from './components'; import { setNotifications, setFieldFormats } from './services'; import { @@ -79,7 +78,6 @@ export class IndexPatternsService { return { ...this.setupApi, indexPatterns: new IndexPatterns(uiSettings, savedObjectsClient, http), - IndexPatternSelect: createIndexPatternSelect(savedObjectsClient), }; } @@ -91,7 +89,6 @@ export class IndexPatternsService { // static code /** @public */ -export { IndexPatternSelect } from './components'; export { CONTAINS_SPACES, getFromSavedObject, @@ -120,4 +117,4 @@ export type IndexPatternsStart = ReturnType; export { IndexPattern, IndexPatterns, StaticIndexPattern, Field, FieldType, FieldListInterface }; /** @public */ -export { getIndexPatternTitle, findIndexPatternByTitle } from './utils'; +export { findIndexPatternByTitle } from './utils'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts index 8542c1dcce24d..8c2878a3ff9ba 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts @@ -71,19 +71,6 @@ export async function findIndexPatternByTitle( ); } -export async function getIndexPatternTitle( - client: SavedObjectsClientContract, - indexPatternId: string -): Promise> { - const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; - - if (savedObject.error) { - throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); - } - - return savedObject.attributes.title; -} - function indexPatternContainsSpaces(indexPattern: string): boolean { return indexPattern.includes(' '); } diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js index 27f37421b0e25..45981adf9af45 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js @@ -17,8 +17,27 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('../../../../../core_plugins/data/public/legacy', () => ({ + indexPatterns: { + indexPatterns: { + get: jest.fn(), + } + } +})); + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js index 663a36ab69f46..c48123f3db714 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js @@ -20,12 +20,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { IndexPatternSelect } from 'ui/index_patterns'; - import { EuiFormRow, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function IndexPatternSelectFormRowUi(props) { const { controlIndex, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index ea029af9e4890..b37e8af0895fe 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -17,12 +17,24 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; + import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js index 5a698d65286ac..8d601f5a727d1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js @@ -17,19 +17,30 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); + import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; -import { - RangeControlEditor, -} from './range_control_editor'; +import { RangeControlEditor } from './range_control_editor'; const controlParams = { id: '1', diff --git a/src/legacy/ui/public/index_patterns/__mocks__/index.ts b/src/legacy/ui/public/index_patterns/__mocks__/index.ts index f51ae86b5c9a7..145045a90ade8 100644 --- a/src/legacy/ui/public/index_patterns/__mocks__/index.ts +++ b/src/legacy/ui/public/index_patterns/__mocks__/index.ts @@ -35,7 +35,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/ui/public/index_patterns/index.ts b/src/legacy/ui/public/index_patterns/index.ts index 690a9cffaa138..d0ff0aaa8c72c 100644 --- a/src/legacy/ui/public/index_patterns/index.ts +++ b/src/legacy/ui/public/index_patterns/index.ts @@ -30,7 +30,6 @@ export const { FieldList, // only used in Discover and StubIndexPattern flattenHitWrapper, formatHitProvider, - IndexPatternSelect, // only used in x-pack/plugin/maps and input control vis } = data.indexPatterns; // static code diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 773d4283cad88..ff89ef69d53ca 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -26,6 +26,10 @@ const mockObservable = () => { }; }; +const mockComponent = () => { + return null; +}; + export const mockUiSettings = { get: (item) => { return mockUiSettings[item]; @@ -139,6 +143,9 @@ export const npStart = { getProvider: sinon.fake(), }, getSuggestions: sinon.fake(), + ui: { + IndexPatternSelect: mockComponent, + }, query: { filterManager: { getFetches$: sinon.fake(), diff --git a/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts new file mode 100644 index 0000000000000..777a12c7e2884 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts @@ -0,0 +1,33 @@ +/* + * 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 { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; + +export async function getIndexPatternTitle( + client: SavedObjectsClientContract, + indexPatternId: string +): Promise> { + const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; + + if (savedObject.error) { + throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); + } + + return savedObject.attributes.title; +} diff --git a/src/plugins/data/public/index_patterns/lib/index.ts b/src/plugins/data/public/index_patterns/lib/index.ts new file mode 100644 index 0000000000000..d1c229513aa33 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getIndexPatternTitle } from './get_index_pattern_title'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index ff5c96c2d89ed..ceb57b4a3a564 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -66,6 +66,9 @@ const createStartContract = (): Start => { search: { search: jest.fn() }, fieldFormats: fieldFormatsMock as FieldFormatsStart, query: queryStartMock, + ui: { + IndexPatternSelect: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 3aa9cd9a0bcb4..d8c45b6786c0c 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -25,6 +25,7 @@ import { getSuggestionsProvider } from './suggestions_provider'; import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; +import { createIndexPatternSelect } from './ui/index_pattern_select'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); @@ -59,6 +60,9 @@ export class DataPublicPlugin implements Plugin; + }; } export * from './autocomplete_provider/types'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 79107d1ede676..cb7c92b00ea3a 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { applyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index.ts b/src/plugins/data/public/ui/index_pattern_select/index.ts similarity index 100% rename from src/legacy/core_plugins/data/public/index_patterns/components/index.ts rename to src/plugins/data/public/ui/index_pattern_select/index.ts diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx rename to src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 77692d7bcaa0d..f868e4b1f7504 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -21,10 +21,10 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../../core/public'; -import { getIndexPatternTitle } from '../utils'; +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { getIndexPatternTitle } from '../../index_patterns/lib'; -interface IndexPatternSelectProps { +export interface IndexPatternSelectProps { onChange: (opt: any) => void; indexPatternId: string; placeholder: string; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 44629d16e6fb3..01c323d73f19e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,7 +16,6 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; @@ -25,6 +24,9 @@ import { indexPatternService, } from '../../../../kibana_services'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + export class JoinExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js index 395b6ac5cc431..3d02b075b3b81 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { RENDER_AS } from './render_as'; import { indexPatternService } from '../../../kibana_services'; @@ -22,6 +21,9 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField({ type }) { return [ES_GEO_FIELD_TYPE.GEO_POINT].includes(type); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js index 9f9789374274a..897ded43be28b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; @@ -20,6 +19,8 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; const GEO_FIELD_TYPES = [ES_GEO_FIELD_TYPE.GEO_POINT]; function filterGeoField({ type }) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js index 61300ed209c1f..a6ba31366d504 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js @@ -9,7 +9,6 @@ import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiSpacer, EuiSwitch, EuiCallOut } from '@elastic/eui'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout'; @@ -19,6 +18,9 @@ import { kfetch } from 'ui/kfetch'; import { ES_GEO_FIELD_TYPE, GIS_API_PATH, ES_SIZE_LIMIT } from '../../../../common/constants'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField(field) { return [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes(field.type); } From 61b3972e5dea889b7cfb4622c89db13c0e3923d5 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 26 Nov 2019 08:01:20 -0700 Subject: [PATCH 43/52] [Maps] refactor static can skip logic from layer class (#51627) --- .../plugins/maps/public/layers/layer.js | 41 --- .../plugins/maps/public/layers/layer.test.js | 113 ------- .../maps/public/layers/util/can_skip_fetch.js | 130 ++++++++ .../public/layers/util/can_skip_fetch.test.js | 287 ++++++++++++++++++ .../maps/public/layers/vector_layer.js | 111 ++----- .../maps/public/layers/vector_layer.test.js | 185 ----------- 6 files changed, 436 insertions(+), 431 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/public/layers/layer.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 72a89046ed2f5..1c2f33df66bf8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -6,8 +6,6 @@ import _ from 'lodash'; import React from 'react'; import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; import { DataRequest } from './util/data_request'; import { MAX_ZOOM, @@ -19,9 +17,6 @@ import uuid from 'uuid/v4'; import { copyPersistentState } from '../reducers/util'; import { i18n } from '@kbn/i18n'; -const SOURCE_UPDATE_REQUIRED = true; -const NO_SOURCE_UPDATE_REQUIRED = false; - export class AbstractLayer { constructor({ layerDescriptor, source }) { @@ -316,42 +311,7 @@ export class AbstractLayer { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } - updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - const { buffer: previousBuffer } = prevMeta; - const { buffer: newBuffer } = nextMeta; - - if (!previousBuffer) { - return SOURCE_UPDATE_REQUIRED; - } - - if (_.isEqual(previousBuffer, newBuffer)) { - return NO_SOURCE_UPDATE_REQUIRED; - } - - const previousBufferGeometry = turf.bboxPolygon([ - previousBuffer.minLon, - previousBuffer.minLat, - previousBuffer.maxLon, - previousBuffer.maxLat - ]); - const newBufferGeometry = turf.bboxPolygon([ - newBuffer.minLon, - newBuffer.minLat, - newBuffer.maxLon, - newBuffer.maxLat - ]); - const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); - - const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); - return doesPreviousBufferContainNewBuffer && !isTrimmed - ? NO_SOURCE_UPDATE_REQUIRED - : SOURCE_UPDATE_REQUIRED; - } getLayerTypeIconName() { throw new Error('should implement Layer#getLayerTypeIconName'); @@ -407,4 +367,3 @@ export class AbstractLayer { } } - diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.test.js b/x-pack/legacy/plugins/maps/public/layers/layer.test.js deleted file mode 100644 index 98be0855cd4b7..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/layer.test.js +++ /dev/null @@ -1,113 +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 { AbstractLayer } from './layer'; - -describe('layer', () => { - const layer = new AbstractLayer({ layerDescriptor: {} }); - - describe('updateDueToExtent', () => { - - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return false; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when buffers are the same', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when meta has no old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when the new buffer is not contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js new file mode 100644 index 0000000000000..610c704b34ec6 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import { isRefreshOnlyQuery } from './is_refresh_only_query'; + +const SOURCE_UPDATE_REQUIRED = true; +const NO_SOURCE_UPDATE_REQUIRED = false; + +export function updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { + const extentAware = source.isFilterByMapBounds(); + if (!extentAware) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const { buffer: previousBuffer } = prevMeta; + const { buffer: newBuffer } = nextMeta; + + if (!previousBuffer) { + return SOURCE_UPDATE_REQUIRED; + } + + if (_.isEqual(previousBuffer, newBuffer)) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const previousBufferGeometry = turf.bboxPolygon([ + previousBuffer.minLon, + previousBuffer.minLat, + previousBuffer.maxLon, + previousBuffer.maxLat + ]); + const newBufferGeometry = turf.bboxPolygon([ + newBuffer.minLon, + newBuffer.minLat, + newBuffer.maxLon, + newBuffer.maxLat + ]); + const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); + + const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); + return doesPreviousBufferContainNewBuffer && !isTrimmed + ? NO_SOURCE_UPDATE_REQUIRED + : SOURCE_UPDATE_REQUIRED; +} + +export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) { + + const timeAware = await source.isTimeAware(); + const refreshTimerAware = await source.isRefreshTimerAware(); + const extentAware = source.isFilterByMapBounds(); + const isFieldAware = source.isFieldAware(); + const isQueryAware = source.isQueryAware(); + const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); + + if ( + !timeAware && + !refreshTimerAware && + !extentAware && + !isFieldAware && + !isQueryAware && + !isGeoGridPrecisionAware + ) { + return (prevDataRequest && prevDataRequest.hasDataOrRequestInProgress()); + } + + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + let updateDueToTime = false; + if (timeAware) { + updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + } + + let updateDueToRefreshTimer = false; + if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { + updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); + } + + let updateDueToFields = false; + if (isFieldAware) { + updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); + } + + let updateDueToQuery = false; + let updateDueToFilters = false; + let updateDueToSourceQuery = false; + let updateDueToApplyGlobalQuery = false; + if (isQueryAware) { + updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; + updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + if (nextMeta.applyGlobalQuery) { + updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); + updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); + } else { + // Global filters and query are not applied to layer search request so no re-fetch required. + // Exception is "Refresh" query. + updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); + } + } + + let updateDueToPrecisionChange = false; + if (isGeoGridPrecisionAware) { + updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); + } + + const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + + const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); + + return !updateDueToTime + && !updateDueToRefreshTimer + && !updateDueToExtentChange + && !updateDueToFields + && !updateDueToQuery + && !updateDueToFilters + && !updateDueToSourceQuery + && !updateDueToApplyGlobalQuery + && !updateDueToPrecisionChange + && !updateDueToSourceMetaChange; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js new file mode 100644 index 0000000000000..77359a6def48f --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -0,0 +1,287 @@ +/* + * 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 { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; +import { DataRequest } from './data_request'; + +describe('updateDueToExtent', () => { + + it('should be false when the source is not extent aware', async () => { + const sourceMock = { + isFilterByMapBounds: () => { return false; } + }; + expect(updateDueToExtent(sourceMock)).toBe(false); + }); + + describe('source is extent aware', () => { + const sourceMock = { + isFilterByMapBounds: () => { return true; } + }; + + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })) + .toBe(false); + }); + + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); + + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent( + sourceMock, + { buffer: oldBuffer, areResultsTrimmed: true }, + { buffer: newBuffer } + )).toBe(true); + }); + + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent(sourceMock)).toBe(true); + }); + + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); + }); + }); +}); + +describe('canSkipSourceUpdate', () => { + const SOURCE_DATA_REQUEST_ID = 'foo'; + + describe('isQueryAware', () => { + + const queryAwareSourceMock = { + isTimeAware: () => { return false; }, + isRefreshTimerAware: () => { return false; }, + isFilterByMapBounds: () => { return false; }, + isFieldAware: () => { return false; }, + isQueryAware: () => { return true; }, + isGeoGridPrecisionAware: () => { return false; }, + }; + const prevFilters = []; + const prevQuery = { + language: 'kuery', + query: 'machine.os.keyword : "win 7"', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' + }; + + describe('applyGlobalQuery is false', () => { + + const prevApplyGlobalQuery = false; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + + describe('applyGlobalQuery is true', () => { + + const prevApplyGlobalQuery = true; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can not skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 9b553803606ed..57126bb7681b8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -18,10 +18,10 @@ import { } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; -import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; +import { canSkipSourceUpdate } from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -229,109 +229,31 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - async _canSkipSourceUpdate(source, sourceDataId, nextMeta) { - const timeAware = await source.isTimeAware(); - const refreshTimerAware = await source.isRefreshTimerAware(); - const extentAware = source.isFilterByMapBounds(); - const isFieldAware = source.isFieldAware(); - const isQueryAware = source.isQueryAware(); - const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); - - if ( - !timeAware && - !refreshTimerAware && - !extentAware && - !isFieldAware && - !isQueryAware && - !isGeoGridPrecisionAware - ) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - return (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress()); - } - - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - if (!sourceDataRequest) { - return false; - } - const prevMeta = sourceDataRequest.getMeta(); - if (!prevMeta) { - return false; - } - - let updateDueToTime = false; - if (timeAware) { - updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - } - - let updateDueToRefreshTimer = false; - if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { - updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); - } - - let updateDueToFields = false; - if (isFieldAware) { - updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); - } - - let updateDueToQuery = false; - let updateDueToFilters = false; - let updateDueToSourceQuery = false; - let updateDueToApplyGlobalQuery = false; - if (isQueryAware) { - updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; - updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); - if (nextMeta.applyGlobalQuery) { - updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); - updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); - } else { - // Global filters and query are not applied to layer search request so no re-fetch required. - // Exception is "Refresh" query. - updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); - } - } - - let updateDueToPrecisionChange = false; - if (isGeoGridPrecisionAware) { - updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); - } - - const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta); - - const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); - - return !updateDueToTime - && !updateDueToRefreshTimer - && !updateDueToExtentChange - && !updateDueToFields - && !updateDueToQuery - && !updateDueToFilters - && !updateDueToSourceQuery - && !updateDueToApplyGlobalQuery - && !updateDueToPrecisionChange - && !updateDueToSourceMetaChange; - } async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - const searchFilters = { ...dataFilters, fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const canSkip = await this._canSkipSourceUpdate(joinSource, sourceDataId, searchFilters); - if (canSkip) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - const propertiesMap = sourceDataRequest ? sourceDataRequest.getData() : null; + const prevDataRequest = this._findDataRequestForSource(sourceDataId); + + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { dataHasChanged: false, join: join, - propertiesMap: propertiesMap + propertiesMap: prevDataRequest.getData() }; } @@ -430,12 +352,17 @@ export class VectorLayer extends AbstractLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); const searchFilters = this._getSearchFilters(dataFilters); - const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters); - if (canSkip) { - const sourceDataRequest = this.getSourceDataRequest(); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkipFetch = await canSkipSourceUpdate({ + source: this._source, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { refreshed: false, - featureCollection: sourceDataRequest.getData() + featureCollection: prevDataRequest.getData() }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js deleted file mode 100644 index 0a07582c57856..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js +++ /dev/null @@ -1,185 +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('./joins/inner_join', () => ({ - InnerJoin: Object -})); - -jest.mock('./tooltips/join_tooltip_property', () => ({ - JoinTooltipProperty: Object -})); - -import { VectorLayer } from './vector_layer'; - -describe('_canSkipSourceUpdate', () => { - const SOURCE_DATA_REQUEST_ID = 'foo'; - - describe('isQueryAware', () => { - - const queryAwareSourceMock = { - isTimeAware: () => { return false; }, - isRefreshTimerAware: () => { return false; }, - isFilterByMapBounds: () => { return false; }, - isFieldAware: () => { return false; }, - isQueryAware: () => { return true; }, - isGeoGridPrecisionAware: () => { return false; }, - }; - const prevFilters = []; - const prevQuery = { - language: 'kuery', - query: 'machine.os.keyword : "win 7"', - queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' - }; - - describe('applyGlobalQuery is false', () => { - - const prevApplyGlobalQuery = false; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - - describe('applyGlobalQuery is true', () => { - - const prevApplyGlobalQuery = true; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can not skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - }); -}); From f7d9e7bbf6ddd494ce94575c10238fea12ea0436 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 26 Nov 2019 08:10:25 -0700 Subject: [PATCH 44/52] [SIEM][Detection Engine] Disambiguates signals, rules, alerts, and detection engine by renaming them (#51684) ## Summary * Renames `signals -> rules` when it is specific about rules * Renames `signals -> detection engine` when is generically talking about both rules and signals * Renames `signals -> alerts` in a few spots when it is talking specifically about alerting plugins * Keeps the name of signal when it involves the signals output index or a source input index for potential signals to be generated from * Did a `git mv ` for everything * Updated local variables as well per rules above. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- ...ls.js => convert_saved_search_to_rules.js} | 26 +- .../plugins/siem/server/kibana.index.ts | 26 +- .../server/lib/detection_engine/README.md | 18 +- .../alerts/__mocks__/es_results.ts | 8 +- .../{create_signals.ts => create_rules.ts} | 6 +- .../{delete_signals.ts => delete_rules.ts} | 18 +- ...ind_signals.test.ts => find_rules.test.ts} | 4 +- .../alerts/{find_signals.ts => find_rules.ts} | 6 +- .../lib/detection_engine/alerts/get_filter.ts | 6 +- ...ead_signals.test.ts => read_rules.test.ts} | 70 ++-- .../alerts/{read_signals.ts => read_rules.ts} | 50 +-- ...nals_alert_type.ts => rules_alert_type.ts} | 8 +- .../lib/detection_engine/alerts/types.ts | 74 ++-- ...e_signals.test.ts => update_rules.test.ts} | 10 +- .../{update_signals.ts => update_rules.ts} | 40 +- .../lib/detection_engine/alerts/utils.test.ts | 94 ++--- .../lib/detection_engine/alerts/utils.ts | 93 +++-- .../routes/__mocks__/request_responses.ts | 12 +- ...ute.test.ts => create_rules_route.test.ts} | 14 +- ...signals_route.ts => create_rules_route.ts} | 28 +- ...ute.test.ts => delete_rules_route.test.ts} | 18 +- ...signals_route.ts => delete_rules_route.ts} | 18 +- ...route.test.ts => find_rules_route.test.ts} | 14 +- ...d_signals_route.ts => find_rules_route.ts} | 20 +- ...route.test.ts => read_rules_route.test.ts} | 12 +- ...d_signals_route.ts => read_rules_route.ts} | 18 +- .../detection_engine/routes/schemas.test.ts | 343 ++++++++---------- .../lib/detection_engine/routes/schemas.ts | 8 +- ...ute.test.ts => update_rules_route.test.ts} | 16 +- ...signals_route.ts => update_rules_route.ts} | 22 +- .../lib/detection_engine/routes/utils.test.ts | 54 +-- .../lib/detection_engine/routes/utils.ts | 66 ++-- .../lib/detection_engine/scripts/README.md | 7 +- ...ls.sh => convert_saved_search_to_rules.sh} | 2 +- ...e_signal_by_id.sh => delete_rule_by_id.sh} | 2 +- ...y_rule_id.sh => delete_rule_by_rule_id.sh} | 2 +- ...al_by_filter.sh => find_rule_by_filter.sh} | 4 +- .../{find_signals.sh => find_rules.sh} | 2 +- ...ind_signals_sort.sh => find_rules_sort.sh} | 2 +- ...{get_signal_by_id.sh => get_rule_by_id.sh} | 2 +- ...l_by_rule_id.sh => get_rule_by_rule_id.sh} | 2 +- .../scripts/{post_signal.sh => post_rule.sh} | 14 +- .../{post_x_signals.sh => post_x_rules.sh} | 4 +- .../filter_with_empty_query.json | 0 .../filter_without_query.json | 0 .../{signals => rules}/root_or_admin_1.json | 0 .../{signals => rules}/root_or_admin_10.json | 0 .../{signals => rules}/root_or_admin_2.json | 0 .../{signals => rules}/root_or_admin_3.json | 0 .../{signals => rules}/root_or_admin_4.json | 0 .../{signals => rules}/root_or_admin_5.json | 0 .../{signals => rules}/root_or_admin_6.json | 0 .../{signals => rules}/root_or_admin_7.json | 0 .../{signals => rules}/root_or_admin_8.json | 0 .../{signals => rules}/root_or_admin_9.json | 0 .../root_or_admin_filter_9998.json | 0 .../root_or_admin_filter_9999.json | 0 .../root_or_admin_meta.json | 0 .../root_or_admin_saved_query_1.json | 0 .../root_or_admin_saved_query_2.json | 0 .../root_or_admin_saved_query_3.json | 0 .../root_or_admin_update_1.json | 0 .../root_or_admin_update_2.json | 0 .../{signals => rules}/watch_longmont.json | 0 .../{update_signal.sh => update_rule.sh} | 14 +- .../legacy/plugins/siem/server/lib/types.ts | 4 +- 66 files changed, 630 insertions(+), 651 deletions(-) rename x-pack/legacy/plugins/siem/scripts/{convert_saved_search_to_signals.js => convert_saved_search_to_rules.js} (84%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{create_signals.ts => create_rules.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{delete_signals.ts => delete_rules.ts} (65%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{find_signals.test.ts => find_rules.test.ts} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{find_signals.ts => find_rules.ts} (88%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{read_signals.test.ts => read_rules.test.ts} (82%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{read_signals.ts => read_rules.ts} (50%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{signals_alert_type.ts => rules_alert_type.ts} (97%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{update_signals.test.ts => update_rules.test.ts} (80%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{update_signals.ts => update_rules.ts} (66%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals_route.test.ts => create_rules_route.test.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals_route.ts => create_rules_route.ts} (72%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{delete_signals_route.test.ts => delete_rules_route.test.ts} (83%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{delete_signals_route.ts => delete_rules_route.ts} (75%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{find_signals_route.test.ts => find_rules_route.test.ts} (89%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{find_signals_route.ts => find_rules_route.ts} (70%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{read_signals_route.test.ts => read_rules_route.test.ts} (88%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{read_signals_route.ts => read_rules_route.ts} (75%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{update_signals_route.test.ts => update_rules_route.test.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{update_signals_route.ts => update_rules_route.ts} (78%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{convert_saved_search_to_signals.sh => convert_saved_search_to_rules.sh} (80%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal_by_id.sh => delete_rule_by_id.sh} (91%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal_by_rule_id.sh => delete_rule_by_rule_id.sh} (89%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signal_by_filter.sh => find_rule_by_filter.sh} (81%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signals.sh => find_rules.sh} (93%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signals_sort.sh => find_rules_sort.sh} (91%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_by_id.sh => get_rule_by_id.sh} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_by_rule_id.sh => get_rule_by_rule_id.sh} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{post_signal.sh => post_rule.sh} (68%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{post_x_signals.sh => post_x_rules.sh} (94%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/filter_with_empty_query.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/filter_without_query.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_10.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_3.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_4.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_5.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_6.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_7.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_8.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_9.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_filter_9998.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_filter_9999.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_meta.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_3.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_update_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_update_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/watch_longmont.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{update_signal.sh => update_rule.sh} (67%) diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js similarity index 84% rename from x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js rename to x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 263a2a59de31f..3e1c5f51ebb5c 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -11,22 +11,22 @@ const path = require('path'); /* * This script is used to parse a set of saved searches on a file system - * and output signal data compatible json files. + * and output rule data compatible json files. * Example: - * node saved_query_to_signals.js ${HOME}/saved_searches ${HOME}/saved_signals + * node saved_query_to_rules.js ${HOME}/saved_searches ${HOME}/saved_rules * - * After editing any changes in the files of ${HOME}/saved_signals/*.json - * you can then post the signals with a CURL post script such as: + * After editing any changes in the files of ${HOME}/saved_rules/*.json + * you can then post the rules with a CURL post script such as: * - * ./post_signal.sh ${HOME}/saved_signals/*.json + * ./post_rule.sh ${HOME}/saved_rules/*.json * * Note: This script is recursive and but does not preserve folder structure - * when it outputs the saved signals. + * when it outputs the saved rules. */ -// Defaults of the outputted signals since the saved KQL searches do not have +// Defaults of the outputted rules since the saved KQL searches do not have // this type of information. You usually will want to make any hand edits after -// doing a search to KQL conversion before posting it as a signal or checking it +// doing a search to KQL conversion before posting it as a rule or checking it // into another repository. const INTERVAL = '5m'; const SEVERITY = 'low'; @@ -36,8 +36,8 @@ const TO = 'now'; const IMMUTABLE = true; const RISK_SCORE = 50; const ENABLED = false; -let allSignals = ''; -const allSignalsNdJson = 'all_rules.ndjson'; +let allRules = ''; +const allRulesNdJson = 'all_rules.ndjson'; // For converting, if you want to use these instead of rely on the defaults then // comment these in and use them for the script. Otherwise this is commented out @@ -74,7 +74,7 @@ const cleanupFileName = file => { async function main() { if (process.argv.length !== 4) { throw new Error( - 'usage: saved_query_to_signals [input directory with saved searches] [output directory]' + 'usage: saved_query_to_rules [input directory with saved searches] [output directory]' ); } @@ -152,11 +152,11 @@ async function main() { `${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2) ); - allSignals += `${JSON.stringify(outputMessage)}\n`; + allRules += `${JSON.stringify(outputMessage)}\n`; } } ); - fs.writeFileSync(`${outputDir}/${allSignalsNdJson}`, allSignals); + fs.writeFileSync(`${outputDir}/${allRulesNdJson}`, allRules); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index a92bca064dab9..2f1530a777042 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,13 +15,13 @@ import { timelineSavedObjectType, } from './saved_objects'; -import { signalsAlertType } from './lib/detection_engine/alerts/signals_alert_type'; +import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; -import { createSignalsRoute } from './lib/detection_engine/routes/create_signals_route'; -import { readSignalsRoute } from './lib/detection_engine/routes/read_signals_route'; -import { findSignalsRoute } from './lib/detection_engine/routes/find_signals_route'; -import { deleteSignalsRoute } from './lib/detection_engine/routes/delete_signals_route'; -import { updateSignalsRoute } from './lib/detection_engine/routes/update_signals_route'; +import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; +import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; +import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; import { ServerFacade } from './types'; const APP_ID = 'siem'; @@ -33,7 +33,7 @@ export const initServerWithKibana = ( ) => { if (kbnServer.plugins.alerting != null) { const version = kbnServer.config().get('pkg.version'); - const type = signalsAlertType({ logger, version }); + const type = rulesAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } @@ -49,13 +49,13 @@ export const initServerWithKibana = ( kbnServer.config().has('xpack.alerting.enabled') === true ) { logger.info( - 'Detected feature flags for actions and alerting and enabling signals API endpoints' + 'Detected feature flags for actions and alerting and enabling detection engine API endpoints' ); - createSignalsRoute(kbnServer); - readSignalsRoute(kbnServer); - updateSignalsRoute(kbnServer); - deleteSignalsRoute(kbnServer); - findSignalsRoute(kbnServer); + createRulesRoute(kbnServer); + readRulesRoute(kbnServer); + updateRulesRoute(kbnServer); + deleteRulesRoute(kbnServer); + findRulesRoute(kbnServer); } const xpackMainPlugin = kbnServer.plugins.xpack_main; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 5d9d87a1cbc2f..4b1dbf62d0dd4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -24,10 +24,10 @@ xpack.alerting.enabled: true xpack.actions.enabled: true ``` -Start Kibana and you will see these messages indicating signals is activated like so: +Start Kibana and you will see these messages indicating detection engine is activated like so: ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` If you see crashes like this: @@ -98,10 +98,10 @@ server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status chan server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -You should also see the SIEM detect the feature flags and start the API endpoints for signals +You should also see the SIEM detect the feature flags and start the API endpoints for detection engine ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same @@ -125,16 +125,16 @@ which will: - Delete any existing alert tasks you have - Delete any existing signal mapping you might have had. - Add the latest signal index and its mappings using your settings from `SIGNALS_INDEX` environment variable. -- Posts the sample signal from `signals/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable -- The sample signal checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit +- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run ```sh -./find_signals.sh +./find_rules.sh ``` -You should see the new signals created like so: +You should see the new rules created like so: ```sh { @@ -184,7 +184,7 @@ Every 5 minutes if you get positive hits you will see messages on info like so: server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 ``` -Signals are space aware and default to the "default" space for these scripts if you do not export +Rules are space aware and default to the "default" space for these scripts if you do not export the variable of SPACE_URL. For example, if you want to post rules to the space `test-space` you would set your SPACE_URL to be: diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 7d3b51c071c09..079d3658461fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, AlertTypeParams } from '../types'; +import { SignalSourceHit, SignalSearchResponse, RuleTypeParams } from '../types'; -export const sampleSignalAlertParams = ( +export const sampleRuleAlertParams = ( maxSignals: number | undefined, riskScore?: number | undefined -): AlertTypeParams => ({ +): RuleTypeParams => ({ ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], @@ -242,4 +242,4 @@ export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearch }, }); -export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts index 8770282356cf5..7c66714484383 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts @@ -5,9 +5,9 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { SignalParams } from './types'; +import { RuleParams } from './types'; -export const createSignals = async ({ +export const createRules = async ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... description, @@ -33,7 +33,7 @@ export const createSignals = async ({ to, type, references, -}: SignalParams) => { +}: RuleParams) => { return alertsClient.create({ data: { name, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts similarity index 65% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts index d89895772f1ef..c3ca1d79424cf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts @@ -4,27 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { readSignals } from './read_signals'; -import { DeleteSignalParams } from './types'; +import { readRules } from './read_rules'; +import { DeleteRuleParams } from './types'; -export const deleteSignals = async ({ +export const deleteRules = async ({ alertsClient, actionsClient, // TODO: Use this when we have actions such as email, etc... id, ruleId, -}: DeleteSignalParams) => { - const signal = await readSignals({ alertsClient, id, ruleId }); - if (signal == null) { +}: DeleteRuleParams) => { + const rule = await readRules({ alertsClient, id, ruleId }); + if (rule == null) { return null; } if (ruleId != null) { - await alertsClient.delete({ id: signal.id }); - return signal; + await alertsClient.delete({ id: rule.id }); + return rule; } else if (id != null) { try { await alertsClient.delete({ id }); - return signal; + return rule; } catch (err) { if (err.output.statusCode === 404) { return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts index 7873781fb05c4..23f031b22a9dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getFilter } from './find_signals'; +import { getFilter } from './find_rules'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { test('it returns a full filter with an AND if sent down', () => { expect(getFilter('alert.attributes.enabled: true')).toEqual( `alert.attributes.alertTypeId: ${SIGNALS_ID} AND alert.attributes.enabled: true` diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts index 63e6a069c0cfe..c1058bd353e8c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts @@ -5,7 +5,7 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { FindSignalParams } from './types'; +import { FindRuleParams } from './types'; export const getFilter = (filter: string | null | undefined) => { if (filter == null) { @@ -15,7 +15,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const findSignals = async ({ +export const findRules = async ({ alertsClient, perPage, page, @@ -23,7 +23,7 @@ export const findSignals = async ({ filter, sortField, sortOrder, -}: FindSignalParams) => { +}: FindRuleParams) => { return alertsClient.find({ options: { fields, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts index 1aa22ea024cc8..5d3b47ecebfd5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -5,7 +5,7 @@ */ import { AlertServices } from '../../../../../alerting/server/types'; -import { SignalAlertParams, PartialFilter } from './types'; +import { RuleAlertParams, PartialFilter } from './types'; import { assertUnreachable } from '../../../utils/build_query'; import { Query, @@ -41,7 +41,7 @@ export const getQueryFilter = ( }; interface GetFilterArgs { - type: SignalAlertParams['type']; + type: RuleAlertParams['type']; filter: Record | undefined | null; filters: PartialFilter[] | undefined | null; language: string | undefined | null; @@ -86,7 +86,7 @@ export const getFilter = async ({ if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index); } else { - // user did not give any additional fall back mechanism for generating a signal + // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring throw err; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts index 39d1fac8f7a09..b3d7ab1322775 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readSignals, readSignalByRuleId, findSignalInArrayByRuleId } from './read_signals'; +import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; import { AlertsClient } from '../../../../../alerting'; import { getResult, @@ -14,19 +14,19 @@ import { } from '../routes/__mocks__/request_responses'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('read_signals', () => { - describe('readSignals', () => { +describe('read_rules', () => { + describe('readRules', () => { test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is set but ruleId is null', async () => { @@ -34,12 +34,12 @@ describe('read_signals', () => { alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { @@ -48,12 +48,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is null but ruleId is set', async () => { @@ -62,12 +62,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return null if id and ruleId are null', async () => { @@ -76,12 +76,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: null, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return null if id and ruleId are undefined', async () => { @@ -90,27 +90,27 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: undefined, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('readSignalByRuleId', () => { + describe('readRuleByRuleId', () => { test('should return a single value if the rule id matches', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should not return a single value if the rule id does not match', async () => { @@ -119,11 +119,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return a single value of rule-1 with multiple values', async () => { @@ -140,11 +140,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(result1); + expect(rule).toEqual(result1); }); test('should return a single value of rule-2 with multiple values', async () => { @@ -161,11 +161,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-2', }); - expect(signal).toEqual(result2); + expect(rule).toEqual(result2); }); test('should return null for a made up value with multiple values', async () => { @@ -182,57 +182,57 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('findSignalInArrayByRuleId', () => { + describe('findRuleInArrayByRuleId', () => { test('returns null if the objects are not of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: 'made up 1', params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('returns correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts similarity index 50% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts index 3c49112aaf50b..5c33526329016 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { findSignals } from './find_signals'; -import { SignalAlertType, isAlertTypeArray, ReadSignalParams, ReadSignalByRuleId } from './types'; +import { findRules } from './find_rules'; +import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; -export const findSignalInArrayByRuleId = ( +export const findRuleInArrayByRuleId = ( objects: object[], ruleId: string -): SignalAlertType | null => { +): RuleAlertType | null => { if (isAlertTypeArray(objects)) { - const signals: SignalAlertType[] = objects; - const signal: SignalAlertType[] = signals.filter(datum => { + const rules: RuleAlertType[] = objects; + const rule: RuleAlertType[] = rules.filter(datum => { return datum.params.ruleId === ruleId; }); - if (signal.length !== 0) { - return signal[0]; + if (rule.length !== 0) { + return rule[0]; } else { return null; } @@ -26,32 +26,32 @@ export const findSignalInArrayByRuleId = ( } }; -// This an extremely slow and inefficient way of getting a signal by its id. -// I have to manually query every single record since the Signal Params are +// This an extremely slow and inefficient way of getting a rule by its id. +// I have to manually query every single record since the rule Params are // not indexed and I cannot push in my own _id when I create an alert at the moment. // TODO: Once we can directly push in the _id, then we should no longer need this way. // TODO: This is meant to be _very_ temporary. -export const readSignalByRuleId = async ({ +export const readRuleByRuleId = async ({ alertsClient, ruleId, -}: ReadSignalByRuleId): Promise => { - const firstSignals = await findSignals({ alertsClient, page: 1 }); - const firstSignal = findSignalInArrayByRuleId(firstSignals.data, ruleId); - if (firstSignal != null) { - return firstSignal; +}: ReadRuleByRuleId): Promise => { + const firstRules = await findRules({ alertsClient, page: 1 }); + const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); + if (firstRule != null) { + return firstRule; } else { - const totalPages = Math.ceil(firstSignals.total / firstSignals.perPage); + const totalPages = Math.ceil(firstRules.total / firstRules.perPage); return Array(totalPages) .fill({}) .map((_, page) => { // page index never starts at zero. It always has to be 1 or greater - return findSignals({ alertsClient, page: page + 1 }); + return findRules({ alertsClient, page: page + 1 }); }) - .reduce>(async (accum, findSignal) => { - const signals = await findSignal; - const signal = findSignalInArrayByRuleId(signals.data, ruleId); - if (signal != null) { - return signal; + .reduce>(async (accum, findRule) => { + const rules = await findRule; + const rule = findRuleInArrayByRuleId(rules.data, ruleId); + if (rule != null) { + return rule; } else { return accum; } @@ -59,7 +59,7 @@ export const readSignalByRuleId = async ({ } }; -export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams) => { +export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { if (id != null) { try { const output = await alertsClient.get({ id }); @@ -73,7 +73,7 @@ export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams } } } else if (ruleId != null) { - return readSignalByRuleId({ alertsClient, ruleId }); + return readRuleByRuleId({ alertsClient, ruleId }); } else { // should never get here, and yet here we are. return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 69eb3eb665060..91d7d18a4945c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -14,17 +14,17 @@ import { import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkCreate } from './utils'; -import { SignalAlertTypeDefinition } from './types'; +import { RuleAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; import { getInputOutputIndex } from './get_input_output_index'; -export const signalsAlertType = ({ +export const rulesAlertType = ({ logger, version, }: { logger: Logger; version: string; -}): SignalAlertTypeDefinition => { +}): RuleAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', @@ -127,7 +127,7 @@ export const signalsAlertType = ({ const bulkIndexResult = await searchAfterAndBulkCreate({ someResult: noReIndexResult, - signalParams: params, + ruleParams: params, services, logger, id: alertId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 29eb7872f163d..28431b8165266 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -21,7 +21,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/server'; export type PartialFilter = Partial; -export interface SignalAlertParams { +export interface RuleAlertParams { description: string; enabled: boolean; falsePositives: string[]; @@ -47,31 +47,31 @@ export interface SignalAlertParams { type: 'filter' | 'query' | 'saved_query'; } -export type SignalAlertParamsRest = Omit< - SignalAlertParams, +export type RuleAlertParamsRest = Omit< + RuleAlertParams, 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' > & { - rule_id: SignalAlertParams['ruleId']; - false_positives: SignalAlertParams['falsePositives']; - saved_id: SignalAlertParams['savedId']; - max_signals: SignalAlertParams['maxSignals']; - risk_score: SignalAlertParams['riskScore']; - output_index: SignalAlertParams['outputIndex']; + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id: RuleAlertParams['savedId']; + max_signals: RuleAlertParams['maxSignals']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; }; -export type OutputSignalAlertRest = SignalAlertParamsRest & { +export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; }; -export type OutputSignalES = OutputSignalAlertRest & { +export type OutputRuleES = OutputRuleAlertRest & { status: 'open' | 'closed'; }; -export type UpdateSignalAlertParamsRest = Partial & { +export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; - rule_id: SignalAlertParams['ruleId'] | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; }; export interface FindParamsRest { @@ -88,18 +88,18 @@ export interface Clients { actionsClient: ActionsClient; } -export type SignalParams = SignalAlertParams & Clients; +export type RuleParams = RuleAlertParams & Clients; -export type UpdateSignalParams = Partial & { +export type UpdateRuleParams = Partial & { id: string | undefined | null; } & Clients; -export type DeleteSignalParams = Clients & { +export type DeleteRuleParams = Clients & { id: string | undefined; ruleId: string | undefined | null; }; -export interface FindSignalsRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; @@ -111,7 +111,7 @@ export interface FindSignalsRequest extends Omit { }; } -export interface FindSignalParams { +export interface FindRuleParams { alertsClient: AlertsClient; perPage?: number; page?: number; @@ -121,34 +121,34 @@ export interface FindSignalParams { sortOrder?: 'asc' | 'desc'; } -export interface ReadSignalParams { +export interface ReadRuleParams { alertsClient: AlertsClient; id?: string | undefined | null; ruleId?: string | undefined | null; } -export interface ReadSignalByRuleId { +export interface ReadRuleByRuleId { alertsClient: AlertsClient; ruleId: string; } -export type AlertTypeParams = Omit; +export type RuleTypeParams = Omit; -export type SignalAlertType = Alert & { +export type RuleAlertType = Alert & { id: string; - params: AlertTypeParams; + params: RuleTypeParams; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalAlertParamsRest; +export interface RulesRequest extends RequestFacade { + payload: RuleAlertParamsRest; } -export interface UpdateSignalsRequest extends RequestFacade { - payload: UpdateSignalAlertParamsRest; +export interface UpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest; } -export type SignalExecutorOptions = Omit & { - params: SignalAlertParams & { +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { scrollSize: number; scrollLock: string; }; @@ -221,24 +221,24 @@ export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -// This returns true because by default a SignalAlertTypeDefinition is an AlertType +// This returns true because by default a RuleAlertTypeDefinition is an AlertType // since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: SignalAlertTypeDefinition): obj is AlertType => { +export const isAlertExecutor = (obj: RuleAlertTypeDefinition): obj is AlertType => { return true; }; -export type SignalAlertTypeDefinition = Omit & { - executor: ({ services, params, state }: SignalExecutorOptions) => Promise; +export type RuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; }; -export const isAlertTypes = (obj: unknown[]): obj is SignalAlertType[] => { - return obj.every(signal => isAlertType(signal)); +export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { + return obj.every(rule => isAlertType(rule)); }; -export const isAlertType = (obj: unknown): obj is SignalAlertType => { +export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isAlertTypeArray = (objArray: unknown[]): objArray is SignalAlertType[] => { +export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { return objArray.length === 0 || isAlertType(objArray[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts index 39f7951a8eab9..1022fea93200f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateName } from './update_signals'; +import { calculateInterval, calculateName } from './update_rules'; -describe('update_signals', () => { +describe('update_rules', () => { describe('#calculateInterval', () => { - test('given a undefined interval, it returns the signalInterval ', () => { + test('given a undefined interval, it returns the ruleInterval ', () => { const interval = calculateInterval(undefined, '10m'); expect(interval).toEqual('10m'); }); - test('given a undefined signalInterval, it returns a undefined interval ', () => { + test('given a undefined ruleInterval, it returns a undefined interval ', () => { const interval = calculateInterval('10m', undefined); expect(interval).toEqual('10m'); }); - test('given both an undefined signalInterval and a undefined interval, it returns 5m', () => { + test('given both an undefined ruleInterval and a undefined interval, it returns 5m', () => { const interval = calculateInterval(undefined, undefined); expect(interval).toEqual('5m'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts similarity index 66% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts index a38fd7756afa1..81360d7824230 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts @@ -6,17 +6,17 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; -import { readSignals } from './read_signals'; -import { UpdateSignalParams } from './types'; +import { readRules } from './read_rules'; +import { UpdateRuleParams } from './types'; export const calculateInterval = ( interval: string | undefined, - signalInterval: string | undefined + ruleInterval: string | undefined ): string => { if (interval != null) { return interval; - } else if (signalInterval != null) { - return signalInterval; + } else if (ruleInterval != null) { + return ruleInterval; } else { return '5m'; } @@ -35,13 +35,13 @@ export const calculateName = ({ return originalName; } else { // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a signal rule name became null or undefined at + // the name of "untitled" just in case a rule name became null or undefined at // some point since TypeScript allows it. return 'untitled'; } }; -export const updateSignal = async ({ +export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types description, @@ -68,17 +68,17 @@ export const updateSignal = async ({ to, type, references, -}: UpdateSignalParams) => { - const signal = await readSignals({ alertsClient, ruleId, id }); - if (signal == null) { +}: UpdateRuleParams) => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { return null; } - // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed + // TODO: Remove this as cast as soon as rule.actions TypeScript bug is fixed // where it is trying to return AlertAction[] or RawAlertAction[] - const actions = (signal.actions as AlertAction[] | undefined) || []; + const actions = (rule.actions as AlertAction[] | undefined) || []; - const params = signal.params || {}; + const params = rule.params || {}; const nextParams = defaults( { @@ -107,18 +107,18 @@ export const updateSignal = async ({ } ); - if (signal.enabled && !enabled) { - await alertsClient.disable({ id: signal.id }); - } else if (!signal.enabled && enabled) { - await alertsClient.enable({ id: signal.id }); + if (rule.enabled && !enabled) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled) { + await alertsClient.enable({ id: rule.id }); } return alertsClient.update({ - id: signal.id, + id: rule.id, data: { tags: [], - name: calculateName({ updatedName: name, originalName: signal.name }), - interval: calculateInterval(interval, signal.interval), + name: calculateName({ updatedName: name, originalName: rule.name }), + interval: calculateInterval(interval, rule.interval), actions, params: nextParams, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index 4aac425c7f80f..19c8d5ccc87ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -17,7 +17,7 @@ import { } from './utils'; import { sampleDocNoSortId, - sampleSignalAlertParams, + sampleRuleAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, sampleDocSearchResultsNoSortIdNoVersion, @@ -25,7 +25,7 @@ import { sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, sampleBulkCreateDuplicateResult, - sampleSignalId, + sampleRuleGuid, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; @@ -52,11 +52,11 @@ describe('utils', () => { describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const fakeSignalSourceHit = buildBulkBody({ doc: sampleDocNoSortId(fakeUuid), - signalParams: sampleParams, - id: sampleSignalId, + ruleParams: sampleParams, + id: sampleRuleGuid, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', @@ -214,7 +214,7 @@ describe('utils', () => { }); test('create successful bulk create', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -227,10 +227,10 @@ describe('utils', () => { }); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -242,7 +242,7 @@ describe('utils', () => { }); test('create successful bulk create with docs with no versioning', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -255,10 +255,10 @@ describe('utils', () => { }); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -269,15 +269,15 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create unsuccessful bulk create due to empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -289,15 +289,15 @@ describe('utils', () => { }); test('create successful bulk create when bulk create has errors', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -312,12 +312,12 @@ describe('utils', () => { describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -326,11 +326,11 @@ describe('utils', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -339,14 +339,14 @@ describe('utils', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -356,13 +356,13 @@ describe('utils', () => { }); describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -375,7 +375,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(30); + const sampleParams = sampleRuleAlertParams(30); const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -409,10 +409,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -426,14 +426,14 @@ describe('utils', () => { }); test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -446,7 +446,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -459,10 +459,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortId(someUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -475,7 +475,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -488,10 +488,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -503,7 +503,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const oneGuid = uuid.v4(); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster @@ -519,10 +519,10 @@ describe('utils', () => { .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -534,7 +534,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -549,10 +549,10 @@ describe('utils', () => { .mockReturnValueOnce(sampleEmptyDocSearchResults); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -564,7 +564,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -581,10 +581,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index f2a3424655945..ba3f310c886ce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -13,13 +13,13 @@ import { SignalSourceHit, SignalSearchResponse, BulkResponse, - AlertTypeParams, - OutputSignalES, + RuleTypeParams, + OutputRuleES, } from './types'; import { buildEventsSearchQuery } from './build_events_query'; interface BuildRuleParams { - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; name: string; id: string; enabled: boolean; @@ -29,46 +29,46 @@ interface BuildRuleParams { } export const buildRule = ({ - signalParams, + ruleParams, name, id, enabled, createdBy, updatedBy, interval, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, status: 'open', - rule_id: signalParams.ruleId, - false_positives: signalParams.falsePositives, - saved_id: signalParams.savedId, - meta: signalParams.meta, - max_signals: signalParams.maxSignals, - risk_score: signalParams.riskScore, - output_index: signalParams.outputIndex, - description: signalParams.description, - filter: signalParams.filter, - from: signalParams.from, - immutable: signalParams.immutable, - index: signalParams.index, + rule_id: ruleParams.ruleId, + false_positives: ruleParams.falsePositives, + saved_id: ruleParams.savedId, + meta: ruleParams.meta, + max_signals: ruleParams.maxSignals, + risk_score: ruleParams.riskScore, + output_index: ruleParams.outputIndex, + description: ruleParams.description, + filter: ruleParams.filter, + from: ruleParams.from, + immutable: ruleParams.immutable, + index: ruleParams.index, interval, - language: signalParams.language, + language: ruleParams.language, name, - query: signalParams.query, - references: signalParams.references, - severity: signalParams.severity, - tags: signalParams.tags, - type: signalParams.type, - to: signalParams.to, + query: ruleParams.query, + references: ruleParams.references, + severity: ruleParams.severity, + tags: ruleParams.tags, + type: ruleParams.type, + to: ruleParams.to, enabled, - filters: signalParams.filters, + filters: ruleParams.filters, created_by: createdBy, updated_by: updatedBy, }); }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { return { parent: { id: doc._id, @@ -83,7 +83,7 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial) interface BuildBulkBodyParams { doc: SignalSourceHit; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; id: string; name: string; createdBy: string; @@ -95,7 +95,7 @@ interface BuildBulkBodyParams { // format search_after result for signals index. export const buildBulkBody = ({ doc, - signalParams, + ruleParams, id, name, createdBy, @@ -104,7 +104,7 @@ export const buildBulkBody = ({ enabled, }: BuildBulkBodyParams): SignalHit => { const rule = buildRule({ - signalParams, + ruleParams, id, name, enabled, @@ -123,7 +123,7 @@ export const buildBulkBody = ({ interface SingleBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -148,7 +148,7 @@ export const generateId = ( // Bulk Index documents. export const singleBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -179,11 +179,11 @@ export const singleBulkCreate = async ({ doc._index, doc._id, doc._version ? doc._version.toString() : '', - signalParams.ruleId ?? '' + ruleParams.ruleId ?? '' ), }, }, - buildBulkBody({ doc, signalParams, id, name, createdBy, updatedBy, interval, enabled }), + buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled }), ]); const time1 = performance.now(); const firstResult: BulkResponse = await services.callCluster('bulk', { @@ -222,7 +222,7 @@ export const singleBulkCreate = async ({ interface SingleSearchAfterParams { searchAfterSortId: string | undefined; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; pageSize: number; @@ -231,7 +231,7 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ searchAfterSortId, - signalParams, + ruleParams, services, logger, pageSize, @@ -241,10 +241,10 @@ export const singleSearchAfter = async ({ } try { const searchAfterQuery = buildEventsSearchQuery({ - index: signalParams.index, - from: signalParams.from, - to: signalParams.to, - filter: signalParams.filter, + index: ruleParams.index, + from: ruleParams.from, + to: ruleParams.to, + filter: ruleParams.filter, size: pageSize, searchAfterSortId, }); @@ -261,7 +261,7 @@ export const singleSearchAfter = async ({ interface SearchAfterAndBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -277,7 +277,7 @@ interface SearchAfterAndBulkCreateParams { // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -296,7 +296,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug('[+] starting bulk insertion'); await singleBulkCreate({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -314,8 +314,7 @@ export const searchAfterAndBulkCreate = async ({ // If the total number of hits for the overall search result is greater than // maxSignals, default to requesting a total of maxSignals, otherwise use the // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = - totalHits >= signalParams.maxSignals ? signalParams.maxSignals : totalHits; + const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -336,7 +335,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ searchAfterSortId: sortId, - signalParams, + ruleParams, services, logger, pageSize, // maximum number of docs to receive per search result. @@ -355,7 +354,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug('next bulk index'); await singleBulkCreate({ someResult: searchAfterResult, - signalParams, + ruleParams, services, logger, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c74d2e87a7ef6..4c49326fbb32a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,13 +6,13 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalAlertParamsRest, SignalAlertType } from '../../alerts/types'; +import { RuleAlertParamsRest, RuleAlertType } from '../../alerts/types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point // when we upgrade and Hapi Server is ok with the filter. -export const typicalPayload = (): Partial> => ({ +export const typicalPayload = (): Partial> => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -28,7 +28,7 @@ export const typicalPayload = (): Partial> language: 'kuery', }); -export const typicalFilterPayload = (): Partial => ({ +export const typicalFilterPayload = (): Partial => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -64,7 +64,7 @@ interface FindHit { page: number; perPage: number; total: number; - data: SignalAlertType[]; + data: RuleAlertType[]; } export const getFindResult = (): FindHit => ({ @@ -81,7 +81,7 @@ export const getFindResultWithSingleHit = (): FindHit => ({ data: [getResult()], }); -export const getFindResultWithMultiHits = (data: SignalAlertType[]): FindHit => ({ +export const getFindResultWithMultiHits = (data: RuleAlertType[]): FindHit => ({ page: 1, perPage: 1, total: 2, @@ -113,7 +113,7 @@ export const createActionResult = (): ActionResult => ({ config: {}, }); -export const getResult = (): SignalAlertType => ({ +export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts index 1232fe3ce219d..4c222c196300c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -10,7 +10,7 @@ import { createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { createSignalsRoute } from './create_signals_route'; +import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -21,17 +21,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('create_signals', () => { +describe('create_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - createSignalsRoute(server); + createRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); @@ -42,14 +42,14 @@ describe('create_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createSignalsRoute(serverWithoutActionClient); + createRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createSignalsRoute(serverWithoutAlertClient); + createRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); @@ -58,7 +58,7 @@ describe('create_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - createSignalsRoute(serverWithoutActionOrAlertClient); + createRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts similarity index 72% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index fa8fd66ef2aef..7e1ac07e1f0aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -9,14 +9,14 @@ import { isFunction } from 'lodash/fp'; import Boom from 'boom'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { createSignals } from '../alerts/create_signals'; -import { SignalsRequest } from '../alerts/types'; -import { createSignalsSchema } from './schemas'; +import { createRules } from '../alerts/create_rules'; +import { RulesRequest } from '../alerts/types'; +import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { transformOrError } from './utils'; -export const createCreateSignalsRoute: Hapi.ServerRoute = { +export const createCreateRulesRoute: Hapi.ServerRoute = { method: 'POST', path: DETECTION_ENGINE_RULES_URL, options: { @@ -25,10 +25,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: createSignalsSchema, + payload: createRulesSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: RulesRequest, headers) { const { description, enabled, @@ -63,13 +63,13 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { } if (ruleId != null) { - const signal = await readSignals({ alertsClient, ruleId }); - if (signal != null) { - return new Boom(`Signal rule_id ${ruleId} already exists`, { statusCode: 409 }); + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); } } - const createdSignal = await createSignals({ + const createdRule = await createRules({ alertsClient, actionsClient, description, @@ -96,10 +96,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { type, references, }); - return transformOrError(createdSignal); + return transformOrError(createdRule); }, }; -export const createSignalsRoute = (server: ServerFacade) => { - server.route(createCreateSignalsRoute); +export const createRulesRoute = (server: ServerFacade) => { + server.route(createCreateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts index 95816aa55d1fe..0808051964dc1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { deleteSignalsRoute } from './delete_signals_route'; +import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -22,12 +22,12 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('delete_signals', () => { +describe('delete_rules', () => { let { server, alertsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient } = createMockServer()); - deleteSignalsRoute(server); + deleteRulesRoute(server); }); afterEach(() => { @@ -35,7 +35,7 @@ describe('delete_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by alertId', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -43,7 +43,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by id', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -51,7 +51,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when deleting a single signal that does not exist with a valid actionClient and alertClient', async () => { + test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -61,14 +61,14 @@ describe('delete_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteSignalsRoute(serverWithoutActionClient); + deleteRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteSignalsRoute(serverWithoutAlertClient); + deleteRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); @@ -77,7 +77,7 @@ describe('delete_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteSignalsRoute(serverWithoutActionOrAlertClient); + deleteRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts index 1f5494a54ddca..12dff0dd60c14 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts @@ -8,13 +8,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { deleteSignals } from '../alerts/delete_signals'; +import { deleteRules } from '../alerts/delete_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; import { getIdError, transformOrError } from './utils'; -export const createDeleteSignalsRoute: Hapi.ServerRoute = { +export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -35,21 +35,21 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await deleteSignals({ + const rule = await deleteRules({ actionsClient, alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const deleteSignalsRoute = (server: ServerFacade): void => { - server.route(createDeleteSignalsRoute); +export const deleteRulesRoute = (server: ServerFacade): void => { + server.route(createDeleteRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts index be3dce36e8716..dae40f05155dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts @@ -11,17 +11,17 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { findSignalsRoute } from './find_signals_route'; +import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient, actionsClient } = createMockServer()); - findSignalsRoute(server); + findRulesRoute(server); }); afterEach(() => { @@ -29,7 +29,7 @@ describe('find_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); actionsClient.find.mockResolvedValue({ page: 1, @@ -44,14 +44,14 @@ describe('find_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - findSignalsRoute(serverWithoutActionClient); + findRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findSignalsRoute(serverWithoutAlertClient); + findRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); @@ -60,7 +60,7 @@ describe('find_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - findSignalsRoute(serverWithoutActionOrAlertClient); + findRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts similarity index 70% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts index 120b71fab7d3a..893fb3f689d16 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { findSignals } from '../alerts/find_signals'; -import { FindSignalsRequest } from '../alerts/types'; -import { findSignalsSchema } from './schemas'; +import { findRules } from '../alerts/find_rules'; +import { FindRulesRequest } from '../alerts/types'; +import { findRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { transformFindAlertsOrError } from './utils'; -export const createFindSignalRoute: Hapi.ServerRoute = { +export const createFindRulesRoute: Hapi.ServerRoute = { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, options: { @@ -22,10 +22,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: findSignalsSchema, + query: findRulesSchema, }, }, - async handler(request: FindSignalsRequest, headers) { + async handler(request: FindRulesRequest, headers) { const { query } = request; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; @@ -34,7 +34,7 @@ export const createFindSignalRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signals = await findSignals({ + const rules = await findRules({ alertsClient, perPage: query.per_page, page: query.page, @@ -42,10 +42,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { sortOrder: query.sort_order, filter: query.filter, }); - return transformFindAlertsOrError(signals); + return transformFindAlertsOrError(rules); }, }; -export const findSignalsRoute = (server: ServerFacade) => { - server.route(createFindSignalRoute); +export const findRulesRoute = (server: ServerFacade) => { + server.route(createFindRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts index 021bcc7b8b48e..47ecf62f41be9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { readSignalsRoute } from './read_signals_route'; +import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -26,7 +26,7 @@ describe('read_signals', () => { beforeEach(() => { ({ server, alertsClient } = createMockServer()); - readSignalsRoute(server); + readRulesRoute(server); }); afterEach(() => { @@ -34,7 +34,7 @@ describe('read_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadRequest()); @@ -43,14 +43,14 @@ describe('read_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - readSignalsRoute(serverWithoutActionClient); + readRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readSignalsRoute(serverWithoutAlertClient); + readRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); @@ -59,7 +59,7 @@ describe('read_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - readSignalsRoute(serverWithoutActionOrAlertClient); + readRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts index 2d662f9049cce..4642c34fbe339 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts @@ -9,12 +9,12 @@ import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { getIdError, transformOrError } from './utils'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; -export const createReadSignalsRoute: Hapi.ServerRoute = { +export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -34,19 +34,19 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { if (!alertsClient || !actionsClient) { return headers.response().code(404); } - const signal = await readSignals({ + const rule = await readRules({ alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const readSignalsRoute = (server: ServerFacade) => { - server.route(createReadSignalsRoute); +export const readRulesRoute = (server: ServerFacade) => { + server.route(createReadRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 6639dc6a3dfd6..6c7e5c4054326 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createSignalsSchema, - updateSignalSchema, - findSignalsSchema, - querySignalSchema, -} from './schemas'; -import { - SignalAlertParamsRest, - FindParamsRest, - UpdateSignalAlertParamsRest, -} from '../alerts/types'; +import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; +import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest } from '../alerts/types'; describe('schemas', () => { - describe('create signals schema', () => { + describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createSignalsSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -32,7 +23,7 @@ describe('schemas', () => { test('[rule_id] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -40,7 +31,7 @@ describe('schemas', () => { test('[rule_id, description] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -49,7 +40,7 @@ describe('schemas', () => { test('[rule_id, description, from] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -59,7 +50,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -70,7 +61,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -82,7 +73,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -95,7 +86,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -109,7 +100,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -124,7 +115,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -140,7 +131,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -158,7 +149,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -176,7 +167,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -195,7 +186,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -215,7 +206,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -233,7 +224,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -252,7 +243,7 @@ describe('schemas', () => { test('If filter type is set then filter is required', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -270,7 +261,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -290,7 +281,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -310,7 +301,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -330,7 +321,7 @@ describe('schemas', () => { test('allows references to be sent as valid', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -351,7 +342,7 @@ describe('schemas', () => { test('defaults references to an array', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -371,8 +362,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { references: number[] } + createRulesSchema.validate< + Partial> & { references: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -394,8 +385,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { index: number[] } + createRulesSchema.validate< + Partial> & { index: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -416,7 +407,7 @@ describe('schemas', () => { test('defaults interval to 5 min', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -433,7 +424,7 @@ describe('schemas', () => { test('defaults max signals to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -451,7 +442,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -471,7 +462,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -489,7 +480,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -508,7 +499,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -528,8 +519,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - createSignalsSchema.validate< - Partial & { filters: string }> + createRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -550,7 +541,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -570,7 +561,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -591,7 +582,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -612,7 +603,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -633,7 +624,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -655,7 +646,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -677,7 +668,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -699,7 +690,7 @@ describe('schemas', () => { test('You can optionally send in an array of tags', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -722,32 +713,32 @@ describe('schemas', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { tags: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - }).error + createRulesSchema.validate> & { tags: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + } + ).error ).toBeTruthy(); }); test('You can optionally send in an array of false positives', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -770,8 +761,8 @@ describe('schemas', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { false_positives: number[] } + createRulesSchema.validate< + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -795,7 +786,7 @@ describe('schemas', () => { test('You can optionally set the immutable to be true', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -818,8 +809,8 @@ describe('schemas', () => { test('You cannot set the immutable to be a number', () => { expect( - createSignalsSchema.validate< - Partial> & { immutable: number } + createRulesSchema.validate< + Partial> & { immutable: number } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -843,7 +834,7 @@ describe('schemas', () => { test('You cannot set the risk_score to 101', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 101, @@ -866,7 +857,7 @@ describe('schemas', () => { test('You cannot set the risk_score to -1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: -1, @@ -889,7 +880,7 @@ describe('schemas', () => { test('You can set the risk_score to 0', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 0, @@ -912,7 +903,7 @@ describe('schemas', () => { test('You can set the risk_score to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 100, @@ -935,7 +926,7 @@ describe('schemas', () => { test('You can set meta to any object you want', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -961,9 +952,7 @@ describe('schemas', () => { test('You cannot create meta as a string', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -987,9 +976,7 @@ describe('schemas', () => { test('You can have an empty query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1013,9 +1000,7 @@ describe('schemas', () => { test('You can omit the query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1038,9 +1023,7 @@ describe('schemas', () => { test('query string defaults to empty string when present with filters', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1062,16 +1045,14 @@ describe('schemas', () => { }); }); - describe('update signals schema', () => { + describe('update rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect( - updateSignalSchema.validate>({}).error - ).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1079,7 +1060,7 @@ describe('schemas', () => { test('[id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', }).error ).toBeFalsy(); @@ -1087,7 +1068,7 @@ describe('schemas', () => { test('[rule_id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeFalsy(); @@ -1095,7 +1076,7 @@ describe('schemas', () => { test('[id and rule_id] does not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'id-1', rule_id: 'rule-1', }).error @@ -1104,7 +1085,7 @@ describe('schemas', () => { test('[rule_id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -1113,7 +1094,7 @@ describe('schemas', () => { test('[id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', }).error @@ -1122,7 +1103,7 @@ describe('schemas', () => { test('[id, risk_score] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', risk_score: 10, }).error @@ -1131,7 +1112,7 @@ describe('schemas', () => { test('[rule_id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1141,7 +1122,7 @@ describe('schemas', () => { test('[id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1151,7 +1132,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1162,7 +1143,7 @@ describe('schemas', () => { test('[id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1173,7 +1154,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1185,7 +1166,7 @@ describe('schemas', () => { test('[id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1197,7 +1178,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1210,7 +1191,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1223,7 +1204,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1237,7 +1218,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1251,7 +1232,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1266,7 +1247,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1281,7 +1262,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1297,7 +1278,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1313,7 +1294,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1330,7 +1311,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1347,7 +1328,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1365,7 +1346,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1383,7 +1364,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1400,7 +1381,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1417,7 +1398,7 @@ describe('schemas', () => { test('If filter type is set then filter is still not required', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1433,7 +1414,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1451,7 +1432,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1469,7 +1450,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1487,7 +1468,7 @@ describe('schemas', () => { test('allows references to be sent as a valid value to update with', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1506,7 +1487,7 @@ describe('schemas', () => { test('does not default references to an array', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1524,7 +1505,7 @@ describe('schemas', () => { test('does not default interval', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1539,7 +1520,7 @@ describe('schemas', () => { test('does not default max signal', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1555,8 +1536,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { references: number[] } + updateRulesSchema.validate< + Partial> & { references: number[] } >({ id: 'rule-1', description: 'some description', @@ -1576,8 +1557,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { index: number[] } + updateRulesSchema.validate< + Partial> & { index: number[] } >({ id: 'rule-1', description: 'some description', @@ -1596,7 +1577,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1614,7 +1595,7 @@ describe('schemas', () => { test('saved_id is not required when type is saved_query and will validate without it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1630,7 +1611,7 @@ describe('schemas', () => { test('saved_id validates with saved_query', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1647,7 +1628,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1665,7 +1646,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1683,7 +1664,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1702,7 +1683,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1721,7 +1702,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1740,7 +1721,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1760,7 +1741,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1780,7 +1761,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1800,7 +1781,7 @@ describe('schemas', () => { test('meta can be updated', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', meta: { whateverYouWant: 'anything_at_all' }, }).error @@ -1809,8 +1790,8 @@ describe('schemas', () => { test('You update meta as a string', () => { expect( - updateSignalSchema.validate< - Partial & { meta: string }> + updateRulesSchema.validate< + Partial & { meta: string }> >({ id: 'rule-1', meta: 'should not work', @@ -1820,8 +1801,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - updateSignalSchema.validate< - Partial & { filters: string }> + updateRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', type: 'query', @@ -1831,14 +1812,14 @@ describe('schemas', () => { }); }); - describe('find signals schema', () => { + describe('find rules schema', () => { test('empty objects do validate', () => { - expect(findSignalsSchema.validate>({}).error).toBeFalsy(); + expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); test('all values validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ per_page: 5, page: 1, sort_field: 'some field', @@ -1851,7 +1832,7 @@ describe('schemas', () => { test('made up parameters do not validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1859,31 +1840,31 @@ describe('schemas', () => { test('per_page validates', () => { expect( - findSignalsSchema.validate>({ per_page: 5 }).error + findRulesSchema.validate>({ per_page: 5 }).error ).toBeFalsy(); }); test('page validates', () => { expect( - findSignalsSchema.validate>({ page: 5 }).error + findRulesSchema.validate>({ page: 5 }).error ).toBeFalsy(); }); test('sort_field validates', () => { expect( - findSignalsSchema.validate>({ sort_field: 'some value' }).error + findRulesSchema.validate>({ sort_field: 'some value' }).error ).toBeFalsy(); }); test('fields validates with a string', () => { expect( - findSignalsSchema.validate>({ fields: ['some value'] }).error + findRulesSchema.validate>({ fields: ['some value'] }).error ).toBeFalsy(); }); test('fields validates with multiple strings', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ fields: ['some value 1', 'some value 2'], }).error ).toBeFalsy(); @@ -1891,23 +1872,23 @@ describe('schemas', () => { test('fields does not validate with a number', () => { expect( - findSignalsSchema.validate> & { fields: number[] }>({ + findRulesSchema.validate> & { fields: number[] }>({ fields: [5], }).error ).toBeTruthy(); }); test('per page has a default of 20', () => { - expect(findSignalsSchema.validate>({}).value.per_page).toEqual(20); + expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); }); test('page has a default of 1', () => { - expect(findSignalsSchema.validate>({}).value.page).toEqual(1); + expect(findRulesSchema.validate>({}).value.page).toEqual(1); }); test('filter works with a string', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ filter: 'some value 1', }).error ).toBeFalsy(); @@ -1915,7 +1896,7 @@ describe('schemas', () => { test('filter does not work with a number', () => { expect( - findSignalsSchema.validate> & { filter: number }>({ + findRulesSchema.validate> & { filter: number }>({ filter: 5, }).error ).toBeTruthy(); @@ -1923,7 +1904,7 @@ describe('schemas', () => { test('sort_order requires sort_field to work', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', }).error ).toBeTruthy(); @@ -1931,7 +1912,7 @@ describe('schemas', () => { test('sort_order and sort_field validate together', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', sort_field: 'some field', }).error @@ -1940,7 +1921,7 @@ describe('schemas', () => { test('sort_order validates with desc and sort_field', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'desc', sort_field: 'some field', }).error @@ -1949,7 +1930,7 @@ describe('schemas', () => { test('sort_order does not validate with a string other than asc and desc', () => { expect( - findSignalsSchema.validate< + findRulesSchema.validate< Partial> & { sort_order: string } >({ sort_order: 'some other string', @@ -1959,29 +1940,27 @@ describe('schemas', () => { }); }); - describe('querySignalSchema', () => { + describe('queryRulesSchema', () => { test('empty objects do not validate', () => { - expect( - querySignalSchema.validate>({}).error - ).toBeTruthy(); + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); test('both rule_id and id being supplied dot not validate', () => { expect( - querySignalSchema.validate>({ rule_id: '1', id: '1' }) + queryRulesSchema.validate>({ rule_id: '1', id: '1' }) .error ).toBeTruthy(); }); test('only id validates', () => { expect( - querySignalSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - querySignalSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 177e7cbebc213..664a98ad7d7dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -52,7 +52,7 @@ const fields = Joi.array() .single(); /* eslint-enable @typescript-eslint/camelcase */ -export const createSignalsSchema = Joi.object({ +export const createRulesSchema = Joi.object({ description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -113,7 +113,7 @@ export const createSignalsSchema = Joi.object({ references: references.default([]), }); -export const updateSignalSchema = Joi.object({ +export const updateRulesSchema = Joi.object({ description, enabled, false_positives, @@ -168,12 +168,12 @@ export const updateSignalSchema = Joi.object({ references, }).xor('id', 'rule_id'); -export const querySignalSchema = Joi.object({ +export const queryRulesSchema = Joi.object({ rule_id, id, }).xor('id', 'rule_id'); -export const findSignalsSchema = Joi.object({ +export const findRulesSchema = Joi.object({ fields, filter: queryFilter, per_page, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts index 7288d18628316..d03d68417dd5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { updateSignalsRoute } from './update_signals_route'; +import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -24,17 +24,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('update_signals', () => { +describe('update_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - updateSignalsRoute(server); + updateRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -43,7 +43,7 @@ describe('update_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when updating a single signal that does not exist', async () => { + test('returns 404 when updating a single rule that does not exist', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -54,14 +54,14 @@ describe('update_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateSignalsRoute(serverWithoutActionClient); + updateRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateSignalsRoute(serverWithoutAlertClient); + updateRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); @@ -70,7 +70,7 @@ describe('update_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - updateSignalsRoute(serverWithoutActionOrAlertClient); + updateRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts index 1dc54f34bd1f7..1cc65054527c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { updateSignal } from '../alerts/update_signals'; -import { UpdateSignalsRequest } from '../alerts/types'; -import { updateSignalSchema } from './schemas'; +import { updateRules } from '../alerts/update_rules'; +import { UpdateRulesRequest } from '../alerts/types'; +import { updateRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { getIdError, transformOrError } from './utils'; -export const createUpdateSignalsRoute: Hapi.ServerRoute = { +export const createUpdateRulesRoute: Hapi.ServerRoute = { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, options: { @@ -22,10 +22,10 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: updateSignalSchema, + payload: updateRulesSchema, }, }, - async handler(request: UpdateSignalsRequest, headers) { + async handler(request: UpdateRulesRequest, headers) { const { description, enabled, @@ -60,7 +60,7 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await updateSignal({ + const rule = await updateRules({ alertsClient, actionsClient, description, @@ -88,14 +88,14 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { type, references, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const updateSignalsRoute = (server: ServerFacade) => { - server.route(createUpdateSignalsRoute); +export const updateRulesRoute = (server: ServerFacade) => { + server.route(createUpdateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index ed9e00735c704..632778d78dab7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { - transformAlertToSignal, + transformAlertToRule, getIdError, transformFindAlertsOrError, transformOrError, @@ -14,11 +14,11 @@ import { import { getResult } from './__mocks__/request_responses'; describe('utils', () => { - describe('transformAlertToSignal', () => { + describe('transformAlertToRule', () => { test('should work with a full data set', () => { - const fullSignal = getResult(); - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -45,8 +45,8 @@ describe('utils', () => { }); test('should work with a partial data set missing data', () => { - const fullSignal = getResult(); - const { from, language, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + const { from, language, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -72,10 +72,10 @@ describe('utils', () => { }); test('should omit query if query is null', () => { - const fullSignal = getResult(); - fullSignal.params.query = null; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -101,10 +101,10 @@ describe('utils', () => { }); test('should omit query if query is undefined', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -130,10 +130,10 @@ describe('utils', () => { }); test('should omit a mix of undefined, null, and missing fields', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - fullSignal.params.language = null; - const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, enabled, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -157,10 +157,10 @@ describe('utils', () => { }); test('should return enabled is equal to false', () => { - const fullSignal = getResult(); - fullSignal.enabled = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: false, @@ -187,10 +187,10 @@ describe('utils', () => { }); test('should return immutable is equal to false', () => { - const fullSignal = getResult(); - fullSignal.params.immutable = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 7b9921b0375d8..eb0ae49436bca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; -import { SignalAlertType, isAlertType, OutputSignalAlertRest, isAlertTypes } from '../alerts/types'; +import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; export const getIdError = ({ id, @@ -26,49 +26,49 @@ export const getIdError = ({ // Transforms the data but will remove any null or undefined it encounters and not include // those on the export -export const transformAlertToSignal = (signal: SignalAlertType): Partial => { - return pickBy((value: unknown) => value != null, { - created_by: signal.createdBy, - description: signal.params.description, - enabled: signal.enabled, - false_positives: signal.params.falsePositives, - filter: signal.params.filter, - filters: signal.params.filters, - from: signal.params.from, - id: signal.id, - immutable: signal.params.immutable, - index: signal.params.index, - interval: signal.interval, - rule_id: signal.params.ruleId, - language: signal.params.language, - output_index: signal.params.outputIndex, - max_signals: signal.params.maxSignals, - risk_score: signal.params.riskScore, - name: signal.name, - query: signal.params.query, - references: signal.params.references, - saved_id: signal.params.savedId, - meta: signal.params.meta, - severity: signal.params.severity, - updated_by: signal.updatedBy, - tags: signal.params.tags, - to: signal.params.to, - type: signal.params.type, +export const transformAlertToRule = (alert: RuleAlertType): Partial => { + return pickBy((value: unknown) => value != null, { + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + false_positives: alert.params.falsePositives, + filter: alert.params.filter, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: alert.params.tags, + to: alert.params.to, + type: alert.params.type, }); }; export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { if (isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(signal => transformAlertToSignal(signal)); + findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; } else { return new Boom('Internal error transforming', { statusCode: 500 }); } }; -export const transformOrError = (signal: unknown): Partial | Boom => { - if (isAlertType(signal)) { - return transformAlertToSignal(signal); +export const transformOrError = (alert: unknown): Partial | Boom => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); } else { return new Boom('Internal error transforming', { statusCode: 500 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md index b3ab0011e1f8f..8d617a8de3fcd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md @@ -4,6 +4,7 @@ search which is not available in the DEV console for the detection engine. Before beginning ensure in your .zshrc/.bashrc you have your user, password, and url set: Open up your .zshrc/.bashrc and add these lines with the variables filled in: + ``` export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} @@ -21,6 +22,7 @@ And that you have the latest version of [NodeJS](https://nodejs.org/en/), [CURL](https://curl.haxx.se), and [jq](https://stedolan.github.io/jq/) installed. If you have homebrew you can install using brew like so + ``` brew install jq ``` @@ -29,10 +31,9 @@ After that you can execute scripts within this folder by first ensuring your current working directory is `./scripts` and then running any scripts within that folder. -Example to add a signal to the system +Example to add a rule to the system ``` cd ./scripts -./post_signal.sh ./signals/root_or_admin_1.json +./post_rule.sh ./rules/root_or_admin_1.json ``` - diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh index 802273c67849d..e4d345eec0b65 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh @@ -9,4 +9,4 @@ set -e ./check_env_variables.sh -node ../../../../scripts/convert_saved_search_to_signals.js $1 $2 +node ../../../../scripts/convert_saved_search_to_rules.js $1 $2 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh index 25cd4bfd33628..2db5740c79bb8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_id.sh ${id} +# Example: ./delete_rule_by_id.sh ${id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh index b74ee260ad8ad..80ef849828b78 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_rule_id.sh ${rule_id} +# Example: ./delete_rule_by_rule_id.sh ${rule_id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh index 34c3c401b4112..34b6208947c57 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh @@ -11,8 +11,8 @@ set -e FILTER=${1:-'alert.attributes.enabled:%20true'} -# Example: ./find_signal_by_filter.sh "alert.attributes.enabled:%20true" -# Example: ./find_signal_by_filter.sh "alert.attributes.name:%20Detect*" +# Example: ./find_rule_by_filter.sh "alert.attributes.enabled:%20true" +# Example: ./find_rule_by_filter.sh "alert.attributes.name:%20Detect*" # The %20 is just an encoded space that is typical of URL's. # Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh index 4542eb7c9a827..520b4afa24cd2 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./find_signals.sh +# Example: ./find_rules.sh curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh index 122f18bbb80e5..8e6690d848db4 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh @@ -12,7 +12,7 @@ set -e SORT=${1:-'enabled'} ORDER=${2:-'asc'} -# Example: ./find_signals_sort.sh enabled asc +# Example: ./find_rules_sort.sh enabled asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh index 239a04846b11a..dba5652390ea9 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_id.sh {rule_id} +# Example: ./get_rule_by_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh index 5100caac32491..114b6570a03e2 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_rule_id.sh {rule_id} +# Example: ./get_rule_by_rule_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh similarity index 68% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh index b8bd0e0e0361f..591cf7625e2e3 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh @@ -10,20 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_1.json}) +RULES=(${@:-./rules/root_or_admin_1.json}) -# Example: ./post_signal.sh -# Example: ./post_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./post_rule.sh +# Example: ./post_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh index abb2111a91c1b..53e7bb504746d 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -12,8 +12,8 @@ set -e # Uses a default of 100 if no argument is specified NUMBER=${1:-100} -# Example: ./post_x_signals.sh -# Example: ./post_x_signals.sh 200 +# Example: ./post_x_rules.sh +# Example: ./post_x_rules.sh 200 for i in $(seq 1 $NUMBER); do { curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh similarity index 67% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh index 04541e1df1fa1..8e1abc7045602 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -10,20 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) +RULES=(${@:-./rules/root_or_admin_update_1.json}) -# Example: ./update_signal.sh -# Example: ./update_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./update_rule.sh +# Example: ./update_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 13d040b969545..9c0059d0d109d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,7 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { SignalAlertParamsRest } from './detection_engine/alerts/types'; +import { RuleAlertParamsRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -66,7 +66,7 @@ export interface SiemContext { } export interface Signal { - rule: Partial; + rule: Partial; parent: { id: string; type: string; From e721ec4ca8d5089d0778579893be0e3c77081f47 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 26 Nov 2019 09:17:06 -0600 Subject: [PATCH 45/52] [APM] Replace StaticIndexPattern with IIndexPattern (#51689) --- .../app/Main/__test__/UpdateBreadcrumbs.test.js | 1 - .../apm/public/components/shared/KueryBar/index.tsx | 11 ++++------- .../plugins/apm/server/lib/helpers/setup_request.ts | 3 +-- .../lib/index_pattern/get_dynamic_index_pattern.ts | 3 +-- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index 8ddf48e79f911..41fb12be284ad 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -10,7 +10,6 @@ import { MemoryRouter } from 'react-router-dom'; import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); const coreMock = { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 24d320505c994..52be4d4fba774 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; @@ -19,7 +18,8 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; import { AutocompleteSuggestion, - AutocompleteProvider + AutocompleteProvider, + IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { usePlugins } from '../../../new-platform/plugin'; @@ -33,10 +33,7 @@ interface State { isLoadingSuggestions: boolean; } -function convertKueryToEsQuery( - kuery: string, - indexPattern: StaticIndexPattern -) { +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { const ast = fromKueryExpression(kuery); return toElasticsearchQuery(ast, indexPattern); } @@ -44,7 +41,7 @@ function convertKueryToEsQuery( function getSuggestions( query: string, selectionStart: number, - indexPattern: StaticIndexPattern, + indexPattern: IIndexPattern, boolFilter: unknown, autocompleteProvider?: AutocompleteProvider ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index 8f19f4baed7ee..a09cdbf91ec6e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -6,7 +6,6 @@ import moment from 'moment'; import { KibanaRequest } from 'src/core/server'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { IIndexPattern } from 'src/plugins/data/common'; import { APMConfig } from '../../../../../../plugins/apm/server'; import { @@ -22,7 +21,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; function decodeUiFilters( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFiltersEncoded?: string ) { if (!uiFiltersEncoded || !indexPattern) { diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index f113e645ed95f..9eb99b7c21e75 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; import { APICaller } from 'src/core/server'; import LRU from 'lru-cache'; import { @@ -51,7 +50,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices }); - const indexPattern: StaticIndexPattern = { + const indexPattern: IIndexPattern = { fields, title: indexPatternTitle }; From 38c17d6c7d03c2697f92f95acceb828fd513bb0c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 26 Nov 2019 10:47:40 -0500 Subject: [PATCH 46/52] Improve session idle timeout, add session lifespan (#49855) This adds an absolute session timeout (lifespan) to user sessions. It also improves the existing session timeout toast and the overall user experience in several ways. --- docs/settings/security-settings.asciidoc | 12 +- .../security/authentication/index.asciidoc | 7 +- docs/user/security/securing-kibana.asciidoc | 27 +- .../resources/bin/kibana-docker | 2 + src/legacy/core_plugins/status_page/index.js | 5 + src/plugins/status_page/kibana.json | 6 + src/plugins/status_page/public/index.ts | 24 + src/plugins/status_page/public/plugin.ts | 39 ++ x-pack/legacy/plugins/security/index.js | 14 +- .../public/hacks/on_session_timeout.js | 14 +- .../public/views/logged_out/logged_out.tsx | 2 +- x-pack/package.json | 1 + x-pack/plugins/security/public/plugin.ts | 24 +- .../public/session/session_expired.test.ts | 97 ++-- .../public/session/session_expired.ts | 9 +- ... => session_idle_timeout_warning.test.tsx} | 8 +- .../session/session_idle_timeout_warning.tsx | 64 +++ .../session/session_lifespan_warning.tsx | 48 ++ .../public/session/session_timeout.mock.ts | 2 + .../public/session/session_timeout.test.tsx | 429 +++++++++++++----- .../public/session/session_timeout.tsx | 200 ++++++-- .../session_timeout_http_interceptor.ts | 4 +- .../session/session_timeout_warning.tsx | 39 -- ...thorized_response_http_interceptor.test.ts | 9 +- x-pack/plugins/security/public/types.ts | 12 + .../authentication/authenticator.test.ts | 340 ++++++++++++-- .../server/authentication/authenticator.ts | 82 +++- .../server/authentication/index.mock.ts | 1 + .../security/server/authentication/index.ts | 14 +- .../authentication/providers/token.test.ts | 10 +- .../server/authentication/providers/token.ts | 20 +- x-pack/plugins/security/server/config.test.ts | 97 ++-- x-pack/plugins/security/server/config.ts | 20 +- x-pack/plugins/security/server/plugin.test.ts | 11 +- x-pack/plugins/security/server/plugin.ts | 10 +- .../server/routes/authentication/index.ts | 2 + .../server/routes/authentication/session.ts | 46 ++ .../translations/translations/ja-JP.json | 5 +- .../translations/translations/zh-CN.json | 5 +- .../api_integration/apis/security/index.js | 1 + .../api_integration/apis/security/session.ts | 87 ++++ x-pack/test/api_integration/config.js | 1 + yarn.lock | 59 ++- 43 files changed, 1524 insertions(+), 385 deletions(-) create mode 100644 src/plugins/status_page/kibana.json create mode 100644 src/plugins/status_page/public/index.ts create mode 100644 src/plugins/status_page/public/plugin.ts rename x-pack/plugins/security/public/session/{session_timeout_warning.test.tsx => session_idle_timeout_warning.test.tsx} (71%) create mode 100644 x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx create mode 100644 x-pack/plugins/security/public/session/session_lifespan_warning.tsx delete mode 100644 x-pack/plugins/security/public/session/session_timeout_warning.tsx create mode 100644 x-pack/plugins/security/public/types.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/session.ts create mode 100644 x-pack/test/api_integration/apis/security/session.ts diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 805d991a9a0f3..a2c05e4d87325 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -49,10 +49,16 @@ is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -`xpack.security.sessionTimeout`:: +`xpack.security.session.idleTimeout`:: Sets the session duration (in milliseconds). By default, sessions stay active -until the browser is closed. When this is set to an explicit timeout, closing the -browser still requires the user to log back in to {kib}. +until the browser is closed. When this is set to an explicit idle timeout, closing +the browser still requires the user to log back in to {kib}. + +`xpack.security.session.lifespan`:: +Sets the maximum duration (in milliseconds), also known as "absolute timeout". By +default, a session can be renewed indefinitely. When this value is set, a session +will end once its lifespan is exceeded, even if the user is not idle. NOTE: if +`idleTimeout` is not set, this setting will still cause sessions to expire. `xpack.security.loginAssistanceMessage`:: Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index c2b1adc5e1b92..e6b70fa059fc2 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -188,9 +188,10 @@ The following sections apply both to <> and <> Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider -for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout` -setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie -can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. +for every request that requires authentication. It also means that the {kib} session depends on the <> settings, and the user is automatically logged +out if the session expires. An access token that is stored in the session cookie can expire, in which case {kib} will +automatically renew it with a one-time-use refresh token and store it in the same cookie. {kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 1c74bd98642a7..2fbc6ba4f1ee6 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -56,16 +56,31 @@ xpack.security.encryptionKey: "something_at_least_32_characters" For more information, see <>. -- -. Optional: Change the default session duration. By default, sessions stay -active until the browser is closed. To change the duration, set the -`xpack.security.sessionTimeout` property in the `kibana.yml` configuration file. -The timeout is specified in milliseconds. For example, set the timeout to 600000 -to expire sessions after 10 minutes: +. Optional: Set a timeout to expire idle sessions. By default, a session stays +active until the browser is closed. To define a sliding session expiration, set +the `xpack.security.session.idleTimeout` property in the `kibana.yml` +configuration file. The idle timeout is specified in milliseconds. For example, +set the idle timeout to 600000 to expire idle sessions after 10 minutes: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.sessionTimeout: 600000 +xpack.security.session.idleTimeout: 600000 +-------------------------------------------------------------------------------- +-- + +. Optional: Change the maximum session duration or "lifespan" -- also known as +the "absolute timeout". By default, a session stays active until the browser is +closed. If an idle timeout is defined, a session can still be extended +indefinitely. To define a maximum session lifespan, set the +`xpack.security.session.lifespan` property in the `kibana.yml` configuration +file. The lifespan is specified in milliseconds. For example, set the lifespan +to 28800000 to expire sessions after 8 hours: ++ +-- +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.session.lifespan: 28800000 -------------------------------------------------------------------------------- -- diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 497307fa4124b..0c8faf47411d4 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,8 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.session.idleTimeout + xpack.security.session.lifespan xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom diff --git a/src/legacy/core_plugins/status_page/index.js b/src/legacy/core_plugins/status_page/index.js index 34de58048b887..9f0ad632fd5b1 100644 --- a/src/legacy/core_plugins/status_page/index.js +++ b/src/legacy/core_plugins/status_page/index.js @@ -26,6 +26,11 @@ export default function (kibana) { hidden: true, url: '/status', }, + injectDefaultVars(server) { + return { + isStatusPageAnonymous: server.config().get('status.allowAnonymous'), + }; + } } }); } diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json new file mode 100644 index 0000000000000..edebf8cb12239 --- /dev/null +++ b/src/plugins/status_page/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "status_page", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/status_page/public/index.ts b/src/plugins/status_page/public/index.ts new file mode 100644 index 0000000000000..db1f05cac076f --- /dev/null +++ b/src/plugins/status_page/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { StatusPagePlugin, StatusPagePluginSetup, StatusPagePluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new StatusPagePlugin(); diff --git a/src/plugins/status_page/public/plugin.ts b/src/plugins/status_page/public/plugin.ts new file mode 100644 index 0000000000000..d072fd4a67c30 --- /dev/null +++ b/src/plugins/status_page/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup } from 'kibana/public'; + +export class StatusPagePlugin implements Plugin { + public setup(core: CoreSetup) { + const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar( + 'isStatusPageAnonymous' + ) as boolean; + + if (isStatusPageAnonymous) { + core.http.anonymousPaths.register('/status'); + } + } + + public start() {} + + public stop() {} +} + +export type StatusPagePluginSetup = ReturnType; +export type StatusPagePluginStart = ReturnType; diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index d147c2572ceeb..60374d562f96c 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -29,7 +29,10 @@ export const security = (kibana) => new kibana.Plugin({ enabled: Joi.boolean().default(true), cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'), encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + session: Joi.object({ + idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + }).default(), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ @@ -44,9 +47,10 @@ export const security = (kibana) => new kibana.Plugin({ }).default(); }, - deprecations: function ({ unused }) { + deprecations: function ({ rename, unused }) { return [ unused('authorization.legacyFallback.enabled'), + rename('sessionTimeout', 'session.idleTimeout'), ]; }, @@ -89,7 +93,11 @@ export const security = (kibana) => new kibana.Plugin({ return { secureCookies: securityPlugin.__legacyCompat.config.secureCookies, - sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout, + session: { + tenant: server.newPlatform.setup.core.http.basePath.serverBasePath, + idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout, + lifespan: securityPlugin.__legacyCompat.config.session.lifespan, + }, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; }, diff --git a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js index 81b14ee7d8bf4..d9fb450779411 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js @@ -7,28 +7,20 @@ import _ from 'lodash'; import { uiModules } from 'ui/modules'; import { isSystemApiRequest } from 'ui/system_api'; -import { Path } from 'plugins/xpack_main/services/path'; import { npSetup } from 'ui/new_platform'; -/** - * Client session timeout is decreased by this number so that Kibana server - * can still access session content during logout request to properly clean - * user session up (invalidate access tokens, redirect to logout portal etc.). - * @type {number} - */ - const module = uiModules.get('security', []); module.config(($httpProvider) => { $httpProvider.interceptors.push(( $q, ) => { - const isUnauthenticated = Path.isUnauthenticated(); + const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname); function interceptorFactory(responseHandler) { return function interceptor(response) { - if (!isUnauthenticated && !isSystemApiRequest(response.config)) { - npSetup.plugins.security.sessionTimeout.extend(); + if (!isAnonymous && !isSystemApiRequest(response.config)) { + npSetup.plugins.security.sessionTimeout.extend(response.config.url); } return responseHandler(response); }; diff --git a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx index 369b531e8ddf8..dbeb68875c1a9 100644 --- a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx +++ b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx @@ -31,7 +31,7 @@ chrome } > - + , diff --git a/x-pack/package.json b/x-pack/package.json index bc7b220bf81f5..eccc5918e6d50 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -210,6 +210,7 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts index 55d125bf993ec..7b1a554e1d3f1 100644 --- a/x-pack/plugins/security/public/plugin.ts +++ b/x-pack/plugins/security/public/plugin.ts @@ -13,6 +13,8 @@ import { } from './session'; export class SecurityPlugin implements Plugin { + private sessionTimeout!: SessionTimeout; + public setup(core: CoreSetup) { const { http, notifications, injectedMetadata } = core; const { basePath, anonymousPaths } = http; @@ -20,23 +22,25 @@ export class SecurityPlugin implements Plugin; diff --git a/x-pack/plugins/security/public/session/session_expired.test.ts b/x-pack/plugins/security/public/session/session_expired.test.ts index 9c0e4cd8036cc..678c397dfbc64 100644 --- a/x-pack/plugins/security/public/session/session_expired.test.ts +++ b/x-pack/plugins/security/public/session/session_expired.test.ts @@ -7,40 +7,81 @@ import { coreMock } from 'src/core/public/mocks'; import { SessionExpired } from './session_expired'; -const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); - -it('redirects user to "/logout" when there is no basePath', async () => { - const { basePath } = coreMock.createSetup().http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); +describe('Session Expiration', () => { + const mockGetItem = jest.fn().mockReturnValue(null); + + beforeAll(() => { + Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: mockGetItem, + }, + writable: true, }); }); - sessionExpired.logout(); + afterAll(() => { + delete (window as any).sessionStorage; + }); + + describe('logout', () => { + const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); + const tenant = ''; - const url = await newUrlPromise; - expect(url).toBe( - `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); -}); + it('redirects user to "/logout" when there is no basePath', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); -it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { - const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); }); - }); - sessionExpired.logout(); + it('adds a provider parameter when an auth provider is saved in sessionStorage', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + mockGetItem.mockReturnValueOnce('basic'); + + sessionExpired.logout(); - const url = await newUrlPromise; - expect(url).toBe( - `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent( + '/foo/bar?baz=quz#quuz' + )}&msg=SESSION_EXPIRED&provider=basic` + ); + }); + + it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { + const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); + }); + }); }); diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 3ef15088bb288..a43da85526757 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -11,14 +11,19 @@ export interface ISessionExpired { } export class SessionExpired { - constructor(private basePath: HttpSetup['basePath']) {} + constructor(private basePath: HttpSetup['basePath'], private tenant: string) {} logout() { const next = this.basePath.remove( `${window.location.pathname}${window.location.search}${window.location.hash}` ); + const key = `${this.tenant}/session_provider`; + const providerName = sessionStorage.getItem(key); + const provider = providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; window.location.assign( - this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED`) + this.basePath.prepend( + `/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED${provider}` + ) ); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx similarity index 71% rename from x-pack/plugins/security/public/session/session_timeout_warning.test.tsx rename to x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx index a52e7ce4e94b5..bb4116420f15d 100644 --- a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning'; -describe('SessionTimeoutWarning', () => { +describe('SessionIdleTimeoutWarning', () => { it('fires its callback when the OK button is clicked', () => { const handler = jest.fn(); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect(handler).toBeCalledTimes(0); wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx new file mode 100644 index 0000000000000..32e4dcc5c6b53 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiProgress } from '@elastic/eui'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + onRefreshSession: () => void; + timeout: number; +} + +export const SessionIdleTimeoutWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+
+ + + +
+ + ); +}; + +export const createToast = (toastLifeTimeMs: number, onRefreshSession: () => void): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'warning', + text: toMountPoint( + + ), + title: i18n.translate('xpack.security.components.sessionIdleTimeoutWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'clock', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_lifespan_warning.tsx b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx new file mode 100644 index 0000000000000..7925e92bce4ed --- /dev/null +++ b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { EuiProgress } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + timeout: number; +} + +export const SessionLifespanWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+ + ); +}; + +export const createToast = (toastLifeTimeMs: number): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'danger', + text: toMountPoint(), + title: i18n.translate('xpack.security.components.sessionLifespanWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'alert', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_timeout.mock.ts b/x-pack/plugins/security/public/session/session_timeout.mock.ts index 9917a50279083..df9b8628b180d 100644 --- a/x-pack/plugins/security/public/session/session_timeout.mock.ts +++ b/x-pack/plugins/security/public/session/session_timeout.mock.ts @@ -8,6 +8,8 @@ import { ISessionTimeout } from './session_timeout'; export function createSessionTimeoutMock() { return { + start: jest.fn(), + stop: jest.fn(), extend: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index 80a22c5fb0b2a..eb947ab95c43b 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -5,6 +5,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import BroadcastChannel from 'broadcast-channel'; import { SessionTimeout } from './session_timeout'; import { createSessionExpiredMock } from './session_expired.mock'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -17,25 +18,46 @@ const expectNoWarningToast = ( expect(notifications.toasts.add).not.toHaveBeenCalled(); }; -const expectWarningToast = ( +const expectIdleTimeoutWarningToast = ( notifications: ReturnType['notifications'], - toastLifeTimeMS: number = 60000 + toastLifeTimeMs: number = 60000 ) => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "text": MountPoint { - "reactNode": , - }, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMS}, - }, - ] - `); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "warning", + "iconType": "clock", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); +}; + +const expectLifespanWarningToast = ( + notifications: ReturnType['notifications'], + toastLifeTimeMs: number = 60000 +) => { + expect(notifications.toasts.add).toHaveBeenCalledTimes(1); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "danger", + "iconType": "alert", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); }; const expectWarningToastHidden = ( @@ -46,128 +68,309 @@ const expectWarningToastHidden = ( expect(notifications.toasts.remove).toHaveBeenCalledWith(toast); }; -describe('warning toast', () => { - test(`shows session expiration warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); +describe('Session Timeout', () => { + const now = new Date().getTime(); + const defaultSessionInfo = { + now, + idleTimeoutExpiration: now + 2 * 60 * 1000, + lifespanExpiration: null, + }; + let notifications: ReturnType['notifications']; + let http: ReturnType['http']; + let sessionExpired: ReturnType; + let sessionTimeout: SessionTimeout; + const toast = Symbol(); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + beforeAll(() => { + BroadcastChannel.enforceOptions({ + type: 'simulate', + }); + Object.defineProperty(window, 'sessionStorage', { + value: { + setItem: jest.fn(() => null), + }, + writable: true, + }); }); - test(`extend delays the warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + beforeEach(() => { + const setup = coreMock.createSetup(); + notifications = setup.notifications; + http = setup.http; + notifications.toasts.add.mockReturnValue(toast as any); + sessionExpired = createSessionExpiredMock(); + const tenant = ''; + sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + // default mocked response for checking session info + http.fetch.mockResolvedValue(defaultSessionInfo); + }); - jest.advanceTimersByTime(1 * 1000); + afterEach(async () => { + jest.clearAllMocks(); + }); - expectWarningToast(notifications); + afterAll(() => { + BroadcastChannel.enforceOptions(null); + delete (window as any).sessionStorage; }); - test(`extend hides displayed warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const toast = Symbol(); - notifications.toasts.add.mockReturnValue(toast as any); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('Lifecycle', () => { + test(`starts and initializes on a non-anonymous path`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).not.toBeUndefined(); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + test(`starts and does not initialize on an anonymous path`, async () => { + http.anonymousPaths.register(window.location.pathname); + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).toBeUndefined(); + expect(http.fetch).not.toHaveBeenCalled(); + }); - sessionTimeout.extend(); - expectWarningToastHidden(notifications, toast); - }); + test(`stops`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + const close = jest.fn(sessionTimeout['channel']!.close); + // eslint-disable-next-line dot-notation + sessionTimeout['channel']!.close = close; + // eslint-disable-next-line dot-notation + const cleanup = jest.fn(sessionTimeout['cleanup']); + // eslint-disable-next-line dot-notation + sessionTimeout['cleanup'] = cleanup; - test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); - - expect(http.get).not.toHaveBeenCalled(); - const toastInput = notifications.toasts.add.mock.calls[0][0]; - expect(toastInput).toHaveProperty('text'); - const mountPoint = (toastInput as any).text; - const wrapper = mountWithIntl(mountPoint.__reactMount__); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(http.get).toHaveBeenCalled(); + sessionTimeout.stop(); + expect(close).toHaveBeenCalled(); + expect(cleanup).toHaveBeenCalled(); + }); }); - test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(64 * 1000, notifications, sessionExpired, http); + describe('API calls', () => { + const methodName = 'handleSessionInfoAndResetTimers'; + let method: jest.Mock; - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expectWarningToast(notifications, 59 * 1000); - }); -}); + beforeEach(() => { + method = jest.fn(sessionTimeout[methodName]); + sessionTimeout[methodName] = method; + }); -describe('session expiration', () => { - test(`expires the session 5 seconds before it really expires`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + test(`handles success`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBe(defaultSessionInfo); + expect(method).toHaveBeenCalledTimes(1); + }); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`handles error`, async () => { + const mockErrorResponse = new Error('some-error'); + http.fetch.mockRejectedValue(mockErrorResponse); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBeUndefined(); + expect(method).not.toHaveBeenCalled(); + }); }); - test(`extend delays the expiration`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('warning toast', () => { + test(`shows idle timeout warning toast`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectIdleTimeoutWarningToast(notifications); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + test(`shows lifespan warning toast`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); + }); + + test(`extend only results in an HTTP call if a warning is shown`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(54 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectNoWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(10 * 1000); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test(`extend does not result in an HTTP call if a lifespan warning is shown`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); - test(`if the session timeout is shorter than 5 seconds, expire session immediately`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(4 * 1000, notifications, sessionExpired, http); + expect(http.fetch).toHaveBeenCalledTimes(1); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`extend hides displayed warning toast`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expectIdleTimeoutWarningToast(notifications); + + http.fetch.mockResolvedValue({ + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + expectWarningToastHidden(notifications, toast); + }); + + test(`extend does nothing for session-related routes`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/internal/security/session'); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test(`checks for updated session info before the warning displays`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we check for updated session info 1 second before the warning is shown + const elapsed = 54 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const toastInput = notifications.toasts.add.mock.calls[0][0]; + expect(toastInput).toHaveProperty('text'); + const mountPoint = (toastInput as any).text; + const wrapper = mountWithIntl(mountPoint.__reactMount__); + wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 64 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalled(); + + jest.advanceTimersByTime(0); + expectIdleTimeoutWarningToast(notifications, 59 * 1000); + }); }); - test(`'null' sessionTimeout never logs you out`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(null, notifications, sessionExpired, http); - sessionTimeout.extend(); - jest.advanceTimersByTime(Number.MAX_VALUE); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + describe('session expiration', () => { + test(`expires the session 5 seconds before it really expires`, async () => { + await sessionTimeout.start(); + + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1 * 1000); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`extend delays the expiration`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + const elapsed = 114 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const sessionInfo = { + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toEqual(sessionInfo); + + // at this point, the session is good for another 120 seconds + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + // because "extend" results in an async request and HTTP call, there is a slight delay when timers are updated + // so we need an extra 100ms of padding for this test to ensure that logout has been called + jest.advanceTimersByTime(1 * 1000 + 100); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`if the session timeout is shorter than 5 seconds, expire session immediately`, async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 4 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(0); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`'null' sessionTimeout never logs you out`, async () => { + http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(Number.MAX_VALUE); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index 32302effd6e46..0069e78b5f372 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NotificationsSetup, Toast, HttpSetup } from 'src/core/public'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; +import { BroadcastChannel } from 'broadcast-channel'; +import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; +import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; +import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -23,58 +23,188 @@ const GRACE_PERIOD_MS = 5 * 1000; */ const WARNING_MS = 60 * 1000; +/** + * Current session info is checked this number of milliseconds before the + * warning toast shows. This will prevent the toast from being shown if the + * session has already been extended. + */ +const SESSION_CHECK_MS = 1000; + +/** + * Route to get session info and extend session expiration + */ +const SESSION_ROUTE = '/internal/security/session'; + export interface ISessionTimeout { - extend(): void; + start(): void; + stop(): void; + extend(url: string): void; } export class SessionTimeout { - private warningTimeoutMilliseconds?: number; - private expirationTimeoutMilliseconds?: number; + private channel?: BroadcastChannel; + private sessionInfo?: SessionInfo; + private fetchTimer?: number; + private warningTimer?: number; + private expirationTimer?: number; private warningToast?: Toast; constructor( - private sessionTimeoutMilliseconds: number | null, private notifications: NotificationsSetup, private sessionExpired: ISessionExpired, - private http: HttpSetup + private http: HttpSetup, + private tenant: string ) {} - extend() { - if (this.sessionTimeoutMilliseconds == null) { + start() { + if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (this.warningTimeoutMilliseconds) { - window.clearTimeout(this.warningTimeoutMilliseconds); + // subscribe to a broadcast channel for session timeout messages + // this allows us to synchronize the UX across tabs and avoid repetitive API calls + const name = `${this.tenant}/session_timeout`; + this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); + this.channel.onmessage = this.handleSessionInfoAndResetTimers; + + // Triggers an initial call to the endpoint to get session info; + // when that returns, it will set the timeout + return this.fetchSessionInfoAndResetTimers(); + } + + stop() { + if (this.channel) { + this.channel.close(); } - if (this.expirationTimeoutMilliseconds) { - window.clearTimeout(this.expirationTimeoutMilliseconds); + this.cleanup(); + } + + /** + * When the user makes an authenticated, non-system API call, this function is used to check + * and see if the session has been extended. + * @param url The URL that was called + */ + extend(url: string) { + // avoid an additional API calls when the user clicks the button on the session idle timeout + if (url.endsWith(SESSION_ROUTE)) { + return; } - if (this.warningToast) { - this.notifications.toasts.remove(this.warningToast); + + const { isLifespanTimeout } = this.getTimeout(); + if (this.warningToast && !isLifespanTimeout) { + // the idle timeout warning is currently showing and the user has clicked elsewhere on the page; + // make a new call to get the latest session info + return this.fetchSessionInfoAndResetTimers(); + } + } + + /** + * Fetch latest session information from the server, and optionally attempt to extend + * the session expiration. + */ + private fetchSessionInfoAndResetTimers = async (extend = false) => { + const method = extend ? 'POST' : 'GET'; + const headers = extend ? {} : { 'kbn-system-api': 'true' }; + try { + const result = await this.http.fetch(SESSION_ROUTE, { method, headers }); + + this.handleSessionInfoAndResetTimers(result); + + // share this updated session info with any other tabs to sync the UX + if (this.channel) { + this.channel.postMessage(result); + } + } catch (err) { + // do nothing; 401 errors will be caught by the http interceptor + } + }; + + /** + * Processes latest session information, and resets timers based on it. These timers are + * used to trigger an HTTP call for updated session information, to show a timeout + * warning, and to log the user out when their session is expired. + */ + private handleSessionInfoAndResetTimers = (sessionInfo: SessionInfo) => { + this.sessionInfo = sessionInfo; + // save the provider name in session storage, we will need it when we log out + const key = `${this.tenant}/session_provider`; + sessionStorage.setItem(key, sessionInfo.provider); + + const { timeout, isLifespanTimeout } = this.getTimeout(); + if (timeout == null) { + return; } - this.warningTimeoutMilliseconds = window.setTimeout( - () => this.showWarning(), - Math.max(this.sessionTimeoutMilliseconds - WARNING_MS - GRACE_PERIOD_MS, 0) + + this.cleanup(); + + // set timers + const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS; + if (timeoutVal > 0 && !isLifespanTimeout) { + // we should check for the latest session info before the warning displays + this.fetchTimer = window.setTimeout(this.fetchSessionInfoAndResetTimers, timeoutVal); + } + this.warningTimer = window.setTimeout( + this.showWarning, + Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0) ); - this.expirationTimeoutMilliseconds = window.setTimeout( + this.expirationTimer = window.setTimeout( () => this.sessionExpired.logout(), - Math.max(this.sessionTimeoutMilliseconds - GRACE_PERIOD_MS, 0) + Math.max(timeout - GRACE_PERIOD_MS, 0) ); - } + }; - private showWarning = () => { - this.warningToast = this.notifications.toasts.add({ - color: 'warning', - text: toMountPoint(), - title: i18n.translate('xpack.security.components.sessionTimeoutWarning.title', { - defaultMessage: 'Warning', - }), - toastLifeTimeMs: Math.min(this.sessionTimeoutMilliseconds! - GRACE_PERIOD_MS, WARNING_MS), - }); + private cleanup = () => { + if (this.fetchTimer) { + window.clearTimeout(this.fetchTimer); + } + if (this.warningTimer) { + window.clearTimeout(this.warningTimer); + } + if (this.expirationTimer) { + window.clearTimeout(this.expirationTimer); + } + if (this.warningToast) { + this.notifications.toasts.remove(this.warningToast); + this.warningToast = undefined; + } }; - private refreshSession = () => { - this.http.get('/api/security/v1/me'); + /** + * Get the amount of time until the session times out, and whether or not the + * session has reached it maximum lifespan. + */ + private getTimeout = (): { timeout: number | null; isLifespanTimeout: boolean } => { + let timeout = null; + let isLifespanTimeout = false; + if (this.sessionInfo) { + const { now, idleTimeoutExpiration, lifespanExpiration } = this.sessionInfo; + if (idleTimeoutExpiration) { + timeout = idleTimeoutExpiration - now; + } + if ( + lifespanExpiration && + (idleTimeoutExpiration === null || lifespanExpiration <= idleTimeoutExpiration) + ) { + timeout = lifespanExpiration - now; + isLifespanTimeout = true; + } + } + return { timeout, isLifespanTimeout }; + }; + + /** + * Show a warning toast depending on the session state. + */ + private showWarning = () => { + const { timeout, isLifespanTimeout } = this.getTimeout(); + const toastLifeTimeMs = Math.min(timeout! - GRACE_PERIOD_MS, WARNING_MS); + let toast: ToastInput; + if (!isLifespanTimeout) { + const refresh = () => this.fetchSessionInfoAndResetTimers(true); + toast = createIdleTimeoutToast(toastLifeTimeMs, refresh); + } else { + toast = createLifespanToast(toastLifeTimeMs); + } + this.warningToast = this.notifications.toasts.add(toast); }; } diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 98516cb4a613b..81625e1753b27 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -24,7 +24,7 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpResponse.request.url); } responseError(httpErrorResponse: HttpErrorResponse) { @@ -45,6 +45,6 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpErrorResponse.request.url); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_timeout_warning.tsx deleted file mode 100644 index e1b4542031ed1..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_warning.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - onRefreshSession: () => void; -} - -export const SessionTimeoutWarning = (props: Props) => { - return ( - <> -

- -

-
- - - -
- - ); -}; diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 6f339a6fc9c95..ff2db01cb6c58 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -25,6 +25,7 @@ const setupHttp = (basePath: string) => { }); return http; }; +const tenant = ''; afterEach(() => { fetchMock.restore(); @@ -32,7 +33,7 @@ afterEach(() => { it(`logs out 401 responses`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const logoutPromise = new Promise(resolve => { jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve()); }); @@ -58,7 +59,7 @@ it(`ignores anonymous paths`, async () => { const http = setupHttp('/foo'); const { anonymousPaths } = http; anonymousPaths.register('/bar'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); @@ -69,7 +70,7 @@ it(`ignores anonymous paths`, async () => { it(`ignores errors which don't have a response, for example network connectivity issues`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); @@ -80,7 +81,7 @@ it(`ignores errors which don't have a response, for example network connectivity it(`ignores requests which omit credentials`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts new file mode 100644 index 0000000000000..e9c4b6e281cf3 --- /dev/null +++ b/x-pack/plugins/security/public/types.ts @@ -0,0 +1,12 @@ +/* + * 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 SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: string; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 78c6feac0fa29..12b4620d554a2 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,11 @@ function getMockOptions(config: Partial = {}) { basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), isSystemAPIRequest: jest.fn(), - config: { sessionTimeout: null, authc: { providers: [], oidc: {}, saml: {} }, ...config }, + config: { + session: { idleTimeout: null, lifespan: null }, + authc: { providers: [], oidc: {}, saml: {} }, + ...config, + }, sessionStorageFactory: sessionStorageMock.createFactory(), }; } @@ -51,7 +55,9 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ authc: { providers: [], oidc: {}, saml: {} } }); + const mockOptions = getMockOptions({ + authc: { providers: [], oidc: {}, saml: {} }, + }); expect(() => new Authenticator(mockOptions)).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); @@ -73,7 +79,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -151,7 +159,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -173,7 +182,12 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.login(request, { provider: 'basic', @@ -286,7 +300,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -344,7 +360,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -366,7 +383,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -381,7 +399,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -400,7 +423,58 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + + it('properly extends session expiration if it is defined.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + + // Create new authenticator with non-null session `idleTimeout`. + mockOptions = getMockOptions({ + session: { + idleTimeout: 3600 * 24, + lifespan: null, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -408,27 +482,39 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: currentDate + 3600 * 24, + lifespanExpiration: null, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('properly extends session timeout if it is defined.', async () => { + it('does not extend session lifespan expiration.', async () => { const user = mockAuthenticatedUser(); const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const hr = 1000 * 60 * 60; - // Create new authenticator with non-null `sessionTimeout`. + // Create new authenticator with non-null session `idleTimeout` and `lifespan`. mockOptions = getMockOptions({ - sessionTimeout: 3600 * 24, + session: { + idleTimeout: hr * 2, + lifespan: hr * 8, + }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) + // it was last extended 1 hour ago, which means it will expire in 1 hour + idleTimeoutExpiration: currentDate + hr * 1, + lifespanExpiration: currentDate + hr * 1.5, + state, + provider: 'basic', + }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); authenticator = new Authenticator(mockOptions); @@ -445,13 +531,69 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: currentDate + 3600 * 24, + idleTimeoutExpiration: currentDate + hr * 2, + lifespanExpiration: currentDate + hr * 1.5, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); + it('only updates the session lifespan expiration if it does not match the current server config.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const hr = 1000 * 60 * 60; + + async function createAndUpdateSession( + lifespan: number | null, + oldExpiration: number | null, + newExpiration: number | null + ) { + mockOptions = getMockOptions({ + session: { + idleTimeout: null, + lifespan, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: 1, + lifespanExpiration: oldExpiration, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: 1, + lifespanExpiration: newExpiration, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + } + // do not change max expiration + createAndUpdateSession(hr * 8, 1234, 1234); + createAndUpdateSession(null, null, null); + // change max expiration + createAndUpdateSession(null, 1234, null); + createAndUpdateSession(hr * 8, null, hr * 8); + }); + it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); @@ -460,7 +602,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -477,7 +624,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -497,7 +649,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -508,7 +661,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -526,7 +680,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -537,7 +692,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -552,7 +708,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -569,7 +730,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -585,7 +751,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.redirectTo('some-url', { state: null }) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); @@ -602,7 +773,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -619,7 +795,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -636,7 +817,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -653,7 +839,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -668,7 +859,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -697,7 +890,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.logout.mockResolvedValue( DeauthenticationResult.redirectTo('some-url') ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const deauthenticationResult = await authenticator.logout(request); @@ -707,10 +905,41 @@ describe('Authenticator', () => { expect(deauthenticationResult.redirectURL).toBe('some-url'); }); + it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic' } }); + mockSessionStorage.get.mockResolvedValue(null); + + mockBasicAuthenticationProvider.logout.mockResolvedValue( + DeauthenticationResult.redirectTo('some-url') + ); + + const deauthenticationResult = await authenticator.logout(request); + + expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + expect(deauthenticationResult.redirected()).toBe(true); + expect(deauthenticationResult.redirectURL).toBe('some-url'); + }); + + it('returns `notHandled` if session does not exist and provider name is invalid', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } }); + mockSessionStorage.get.mockResolvedValue(null); + + const deauthenticationResult = await authenticator.logout(request); + + expect(deauthenticationResult.notHandled()).toBe(true); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const deauthenticationResult = await authenticator.logout(request); @@ -719,4 +948,51 @@ describe('Authenticator', () => { expect(deauthenticationResult.notHandled()).toBe(true); }); }); + + describe('`getSessionInfo` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType; + let mockSessionStorage: jest.Mocked>; + beforeEach(() => { + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + }); + + it('returns current session info if session exists.', async () => { + const request = httpServerMock.createKibanaRequest(); + const state = { authorization: 'Basic xxx' }; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const mockInfo = { + now: currentDate, + idleTimeoutExpiration: currentDate + 60000, + lifespanExpiration: currentDate + 120000, + provider: 'basic', + }; + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, + lifespanExpiration: mockInfo.lifespanExpiration, + state, + provider: mockInfo.provider, + }); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toEqual(mockInfo); + }); + + it('returns `null` if session does not exist.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 18bdc9624b12b..17a773c6b6e8c 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -31,6 +31,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; +import { SessionInfo } from '../../public/types'; /** * The shape of the session that is actually stored in the cookie. @@ -45,7 +46,13 @@ export interface ProviderSession { * The Unix time in ms when the session should be considered expired. If `null`, session will stay * active until the browser is closed. */ - expires: number | null; + idleTimeoutExpiration: number | null; + + /** + * The Unix time in ms which is the max total lifespan of the session. If `null`, session expire + * time can be extended indefinitely. + */ + lifespanExpiration: number | null; /** * Session value that is fed to the authentication provider. The shape is unknown upfront and @@ -77,7 +84,7 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - config: Pick; + config: Pick; basePath: HttpServiceSetup['basePath']; loggers: LoggerFactory; clusterClient: IClusterClient; @@ -153,9 +160,14 @@ export class Authenticator { private readonly providers: Map; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. + * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly ttl: number | null = null; + private readonly idleTimeout: number | null = null; + + /** + * Session max lifespan in ms. If `null` session may live indefinitely. + */ + private readonly lifespan: number | null = null; /** * Internal authenticator logger. @@ -202,7 +214,9 @@ export class Authenticator { }) ); - this.ttl = this.options.config.sessionTimeout; + // only set these vars if they are defined in options (otherwise coalesce to existing/default) + this.idleTimeout = this.options.config.session.idleTimeout; + this.lifespan = this.options.config.session.lifespan; } /** @@ -257,10 +271,12 @@ export class Authenticator { if (existingSession && shouldClearSession) { sessionStorage.clear(); } else if (!attempt.stateless && authenticationResult.shouldUpdateState()) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.state, provider: attempt.provider, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } @@ -315,10 +331,18 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const sessionValue = await this.getSessionValue(sessionStorage); + const providerName = this.getProviderName(request.query); if (sessionValue) { sessionStorage.clear(); return this.providers.get(sessionValue.provider)!.logout(request, sessionValue.state); + } else if (providerName) { + // provider name is passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it + const provider = this.providers.get(providerName); + if (provider) { + return provider.logout(request, null); + } } // Normally when there is no active session in Kibana, `logout` method shouldn't do anything @@ -334,6 +358,29 @@ export class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Returns session information for the current request. + * @param request Request instance. + */ + async getSessionInfo(request: KibanaRequest): Promise { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const sessionValue = await this.getSessionValue(sessionStorage); + + if (sessionValue) { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + return { + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + }; + } + return null; + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. @@ -410,13 +457,34 @@ export class Authenticator { ) { sessionStorage.clear(); } else if (sessionCanBeUpdated) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, provider: providerType, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } } + + private getProviderName(query: any): string | null { + if (query && query.provider && typeof query.provider === 'string') { + return query.provider; + } + return null; + } + + private calculateExpiry( + existingSession: ProviderSession | null + ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { + let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; + if (existingSession && existingSession.lifespanExpiration && this.lifespan) { + lifespanExpiration = existingSession.lifespanExpiration; + } + const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + + return { idleTimeoutExpiration, lifespanExpiration }; + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index dcaf26f53fe01..77f1f9e45aea7 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -14,5 +14,6 @@ export const authenticationMock = { invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), logout: jest.fn(), + getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index df16dd375e858..2e67a0eaaa6d5 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -68,15 +68,22 @@ export async function setupAuthentication({ const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, - config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, + config: { session: config.session, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, - validate: (sessionValue: ProviderSession) => - !(sessionValue.expires && sessionValue.expires < Date.now()), + validate: (sessionValue: ProviderSession) => { + const { idleTimeoutExpiration, lifespanExpiration } = sessionValue; + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; + }, }), }); @@ -151,6 +158,7 @@ export async function setupAuthentication({ return { login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), + getSessionInfo: authenticator.getSessionInfo.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 8eb20447c7e2c..a6850dcdf8321 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -422,20 +422,16 @@ describe('TokenAuthenticationProvider', () => { }); describe('`logout` method', () => { - it('returns `notHandled` if state is not presented.', async () => { + it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); sinon.assert.notCalled(mockOptions.tokens.invalidate); - - deauthenticateResult = await provider.logout(request, tokenPair); - expect(deauthenticateResult.notHandled()).toBe(false); }); it('fails if `tokens.invalidate` fails', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index d1881ad4b5498..c5f8f07e50b11 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -120,18 +120,16 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async logout(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to log user out via ${request.url.path}.`); - if (!state) { + if (state) { + this.logger.debug('Token-based logout has been initiated by the user.'); + try { + await this.options.tokens.invalidate(state); + } catch (err) { + this.logger.debug(`Failed invalidating user's access token: ${err.message}`); + return DeauthenticationResult.failed(err); + } + } else { this.logger.debug('There are no access and refresh tokens to invalidate.'); - return DeauthenticationResult.notHandled(); - } - - this.logger.debug('Token-based logout has been initiated by the user.'); - - try { - await this.options.tokens.invalidate(state); - } catch (err) { - this.logger.debug(`Failed invalidating user's access token: ${err.message}`); - return DeauthenticationResult.failed(err); } const queryString = request.url.search || `?msg=LOGGED_OUT`; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 569611516c880..9ddb3e6e96b90 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,48 +13,57 @@ import { createConfig$, ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { @@ -253,7 +262,11 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16), secureCookies: true }); + expect(config).toEqual({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + session: { idleTimeout: null, lifespan: null }, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -273,7 +286,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: false }); + expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -293,7 +306,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -313,7 +326,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index a257a25344393..c7d990f81369e 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -34,7 +34,11 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + session: schema.object({ + idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ providers: schema.arrayOf(schema.string(), { defaultValue: ['basic'], minSize: 1 }), @@ -83,11 +87,23 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b secureCookies = true; } - return { + // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" + // however, NP does not yet have a mechanism to automatically rename deprecated keys + // for the time being, we'll do it manually: + const sess = config.session; + const session = { + idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, + lifespan: (sess && sess.lifespan) || null, + }; + + const val = { ...config, encryptionKey, secureCookies, + session, }; + delete val.sessionTimeout; // DEPRECATED + return val; }) ); } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 2ff0e915fc1b0..26788c3ef9230 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -20,7 +20,10 @@ describe('Security Plugin', () => { plugin = new Plugin( coreMock.createPluginInitializerContext({ cookieName: 'sid', - sessionTimeout: 1500, + session: { + idleTimeout: 1500, + lifespan: null, + }, authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, @@ -54,7 +57,10 @@ describe('Security Plugin', () => { "cookieName": "sid", "loginAssistanceMessage": undefined, "secureCookies": true, - "sessionTimeout": 1500, + "session": Object { + "idleTimeout": 1500, + "lifespan": null, + }, }, "license": Object { "getFeatures": [Function], @@ -66,6 +72,7 @@ describe('Security Plugin', () => { "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], + "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], "login": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index c8761050524a5..e956603517349 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -74,7 +74,10 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - sessionTimeout: number | null; + session: { + idleTimeout: number | null; + lifespan: number | null; + }; secureCookies: boolean; authc: { providers: string[] }; }>; @@ -206,7 +209,10 @@ export class Plugin { // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { loginAssistanceMessage: config.loginAssistanceMessage, - sessionTimeout: config.sessionTimeout, + session: { + idleTimeout: config.session.idleTimeout, + lifespan: config.session.lifespan, + }, secureCookies: config.secureCookies, cookieName: config.cookieName, authc: { providers: config.authc.providers }, diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 0e3f03255dcb9..086647dcb3459 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; import { RouteDefinitionParams } from '..'; export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + defineSessionRoutes(params); if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authentication/session.ts b/x-pack/plugins/security/server/routes/authentication/session.ts new file mode 100644 index 0000000000000..cdebc19d7cf8d --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/session.ts @@ -0,0 +1,46 @@ +/* + * 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 { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for all authentication realms. + */ +export function defineSessionRoutes({ router, logger, authc, basePath }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, request, response) => { + try { + const sessionInfo = await authc.getSessionInfo(request); + // This is an authenticated request, so sessionInfo will always be non-null. + return response.ok({ body: sessionInfo! }); + } catch (err) { + logger.error(`Error retrieving user session: ${err.message}`); + return response.internalError(); + } + } + ); + + router.post( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, _request, response) => { + // We can't easily return updated session info in a single HTTP call, because session data is obtained from + // the HTTP request, not the response. So the easiest way to facilitate this is to redirect the client to GET + // the session endpoint after the client's session has been extended. + return response.redirected({ + headers: { + location: `${basePath.serverBasePath}/internal/security/session`, + }, + }); + } + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f83d0c9ea3c9a..f5fc453557122 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9929,9 +9929,8 @@ "xpack.security.account.passwordsDoNotMatch": "パスワードが一致していません。", "xpack.security.account.usernameGroupDescription": "この情報は変更できません。", "xpack.security.account.usernameGroupTitle": "ユーザー名とメールアドレス", - "xpack.security.components.sessionTimeoutWarning.message": "操作がないため間もなくログアウトします。再開するには [OK] をクリックしてくださ。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "ログイン", "xpack.security.loggedOut.title": "ログアウト完了", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワード再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a830eaacd29e3..288fc92be3cbd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10018,9 +10018,8 @@ "xpack.security.account.passwordsDoNotMatch": "密码不匹配。", "xpack.security.account.usernameGroupDescription": "不能更改此信息。", "xpack.security.account.usernameGroupTitle": "用户名和电子邮件", - "xpack.security.components.sessionTimeoutWarning.message": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "登录", "xpack.security.loggedOut.title": "已成功退出", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。", diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 4d034622427fc..052d984774e69 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./session')); }); } diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts new file mode 100644 index 0000000000000..7c7883f58cb30 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Cookie, cookie } from 'request'; +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + describe('Session', () => { + let sessionCookie: Cookie; + + const saveCookie = async (response: any) => { + // save the response cookie, and pass back the result + sessionCookie = cookie(response.headers['set-cookie'][0])!; + return response; + }; + const getSessionInfo = async () => + supertest + .get('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-api', 'true') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(200); + const extendSession = async () => + supertest + .post('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(302) + .then(saveCookie); + + beforeEach(async () => { + await supertest + .post('/api/security/v1/login') + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204) + .then(saveCookie); + }); + + describe('GET /internal/security/session', () => { + it('should return current session information', async () => { + const { body } = await getSessionInfo(); + expect(body.now).to.be.a('number'); + expect(body.idleTimeoutExpiration).to.be.a('number'); + expect(body.lifespanExpiration).to.be(null); + expect(body.provider).to.be('basic'); + }); + + it('should not extend the session', async () => { + const { body } = await getSessionInfo(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + }); + }); + + describe('POST /internal/security/session', () => { + it('should redirect to GET', async () => { + const response = await extendSession(); + expect(response.headers.location).to.be('/internal/security/session'); + }); + + it('should extend the session', async () => { + // browsers will follow the redirect and return the new session info, but this testing framework does not + // we simulate that behavior in this test by sending another GET request + const { body } = await getSessionInfo(); + await extendSession(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 64a9cafca406a..9c67dfe61b957 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -21,6 +21,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', ], }, diff --git a/yarn.lock b/yarn.lock index e30abf76145a3..7e965979fd46f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1004,7 +1004,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -6448,6 +6448,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + big-time@2.x.x: version "2.0.1" resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" @@ -6779,6 +6784,19 @@ brfs@^2.0.2: static-module "^3.0.2" through2 "^2.0.0" +broadcast-channel@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.0.3.tgz#e6668693af410f7dda007fd6f80e21992d51f3cc" + integrity sha512-ogRIiGDL0bdeOzPO13YQKX12IvRBDOxej2CJaEwuEOF011C9JBABz+8MJ/WZ34eGbXGrfVBeeeaMTWjBzxVKkw== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "3.0.0" + unload "2.2.0" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -16938,6 +16956,11 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" integrity sha512-/812MXr9RBtMObviZ8gQBhHO8MOrGj8HlEE+4ccMTElNA/6I3u39u+bhny55Lk921yn44nSZFy9naNLElL5wgQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -18997,6 +19020,11 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha1-R9x7z2IXG4Aw4hUv2C8SpolKcRk= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -19577,6 +19605,13 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -24453,6 +24488,13 @@ rimraf@2.6.3, rimraf@^2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.0, rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -24460,13 +24502,6 @@ rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" - integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== - dependencies: - glob "^7.1.3" - rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -28185,6 +28220,14 @@ unlazy-loader@^0.1.3: dependencies: requires-regex "^0.3.3" +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 1fb779b7d03a4bc970f1d987080b524d23fdec52 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 26 Nov 2019 11:34:03 -0500 Subject: [PATCH 47/52] Skipped these tests because their apps are not enabled on cloud. (#51677) --- x-pack/test/functional/apps/cross_cluster_replication/index.ts | 2 +- x-pack/test/functional/apps/remote_clusters/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 21fc1982edc42..efcfaaba6037c 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index dc47bd9de3815..9a4bc5b6a5cbd 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; From 074f24ee32c5ef59c49eb89856e0502eb15f3d75 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 26 Nov 2019 18:03:16 +0100 Subject: [PATCH 48/52] [APM] Fix watcher integration (#51721) Closes #51720. --- .../ServiceIntegrations/WatcherFlyout.tsx | 1 + .../__test__/createErrorGroupWatch.test.ts | 20 +++++++++++++------ .../createErrorGroupWatch.ts | 10 ++++++++-- .../apm/public/services/rest/callApi.ts | 7 +++---- .../services/rest/{watcher.js => watcher.ts} | 15 +++++++++++--- 5 files changed, 38 insertions(+), 15 deletions(-) rename x-pack/legacy/plugins/apm/public/services/rest/{watcher.js => watcher.ts} (60%) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index d52c869b95872..18964531958f7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -191,6 +191,7 @@ export class WatcherFlyout extends Component< ) as string; return createErrorGroupWatch({ + http: core.http, emails, schedule, serviceName, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index c7860b81a7b1e..f05d343ad7ba5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -7,22 +7,30 @@ import { isArray, isObject, isString } from 'lodash'; import mustache from 'mustache'; import uuid from 'uuid'; -// @ts-ignore import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; +import { HttpServiceBase } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; +jest.mock('../../../../../services/rest/callApi', () => ({ + callApi: () => Promise.resolve(null) +})); + describe('createErrorGroupWatch', () => { let createWatchResponse: string; let tmpl: any; + const createWatchSpy = jest + .spyOn(rest, 'createWatch') + .mockResolvedValue(undefined); + beforeEach(async () => { jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); - jest.spyOn(rest, 'createWatch').mockReturnValue(undefined); createWatchResponse = await createErrorGroupWatch({ + http: {} as HttpServiceBase, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { @@ -36,19 +44,19 @@ describe('createErrorGroupWatch', () => { apmIndexPatternTitle: 'myIndexPattern' }); - const watchBody = rest.createWatch.mock.calls[0][1]; + const watchBody = createWatchSpy.mock.calls[0][0].watch; const templateCtx = { payload: esResponse, metadata: watchBody.metadata }; - tmpl = renderMustache(rest.createWatch.mock.calls[0][1], templateCtx); + tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); }); afterEach(() => jest.restoreAllMocks()); it('should call createWatch with correct args', () => { - expect(rest.createWatch.mock.calls[0][0]).toBe('apm-mocked-uuid'); + expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid'); }); it('should format slack message correctly', () => { @@ -78,7 +86,7 @@ describe('createErrorGroupWatch', () => { }); it('should return watch id', async () => { - const id = rest.createWatch.mock.calls[0][0]; + const id = createWatchSpy.mock.calls[0][0].id; expect(createWatchResponse).toEqual(id); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index e7d06403b8f8e..1d21e35f122d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; +import { HttpServiceBase } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -17,7 +18,6 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; -// @ts-ignore import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { @@ -35,6 +35,7 @@ export interface Schedule { } interface Arguments { + http: HttpServiceBase; emails: string[]; schedule: Schedule; serviceName: string; @@ -54,6 +55,7 @@ interface Actions { } export async function createErrorGroupWatch({ + http, emails = [], schedule, serviceName, @@ -250,6 +252,10 @@ export async function createErrorGroupWatch({ }; } - await createWatch(id, body); + await createWatch({ + http, + id, + watch: body + }); return id; } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index e1b61d06e3559..887200bdfc22a 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -9,10 +9,11 @@ import LRU from 'lru-cache'; import hash from 'object-hash'; import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; -export type FetchOptions = HttpFetchOptions & { +export type FetchOptions = Omit & { pathname: string; forceCache?: boolean; method?: string; + body?: any; }; function fetchOptionsWithDebug(fetchOptions: FetchOptions) { @@ -26,9 +27,7 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) { const body = isGet ? {} : { - body: JSON.stringify( - fetchOptions.body || ({} as HttpFetchOptions['body']) - ) + body: JSON.stringify(fetchOptions.body || {}) }; return { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts similarity index 60% rename from x-pack/legacy/plugins/apm/public/services/rest/watcher.js rename to x-pack/legacy/plugins/apm/public/services/rest/watcher.ts index 9d68a1665912c..dfa64b5368ee9 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpServiceBase } from 'kibana/public'; import { callApi } from './callApi'; -export async function createWatch(id, watch) { - return callApi({ +export async function createWatch({ + id, + watch, + http +}: { + http: HttpServiceBase; + id: string; + watch: any; +}) { + return callApi(http, { method: 'PUT', pathname: `/api/watcher/watch/${id}`, - body: JSON.stringify({ type: 'json', id, watch }) + body: { type: 'json', id, watch } }); } From c8f0a751a775b2dbe1f8f26629ed00326a4a3479 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 26 Nov 2019 12:20:37 -0500 Subject: [PATCH 49/52] [Timepicker] Ensure we filter out undefined values (#51458) * Fix error with undefined from or to * PR feedback * Remove unnecessary test --- src/plugins/data/public/query/timefilter/time_history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index 4dabbb557e9db..fe73fd85b164d 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -37,7 +37,7 @@ export class TimeHistory { } add(time: TimeRange) { - if (!time) { + if (!time || !time.from || !time.to) { return; } From 84489619bbc1f7d0e0d6104e0248116460074227 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 26 Nov 2019 18:21:02 +0100 Subject: [PATCH 50/52] Upgrade typescript-eslint to 2.9.0 (#51737) * Upgrade typescript-eslint to 2.9.0 * Remove redundant APM eslint disable --- package.json | 4 +- packages/eslint-config-kibana/package.json | 4 +- .../avg_duration_by_browser/transformer.ts | 2 - yarn.lock | 40 +++++++++---------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a4f7b869aef6f..ce43089105268 100644 --- a/package.json +++ b/package.json @@ -349,8 +349,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 71517bc10404d..ee65a1cf79148 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts index 805f8f192bdb1..5d140155f75e4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts @@ -14,8 +14,6 @@ export function transformer({ response: ESResponse; }): AvgDurationByBrowserAPIResponse { const allUserAgentKeys = new Set( - // TODO(TS-3.7-ESLINT) - // eslint-disable-next-line @typescript-eslint/camelcase (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) => key.toString() ) diff --git a/yarn.lock b/yarn.lock index 7e965979fd46f..1cf41a3ecd57c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4154,24 +4154,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz#eca584d46094ebebc3cb3e9fb625bfbc904a534d" - integrity sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA== +"@typescript-eslint/eslint-plugin@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz#fa810282c0e45f6c2310b9c0dfcd25bff97ab7e9" + integrity sha512-98rfOt3NYn5Gr9wekTB8TexxN6oM8ZRvYuphPs1Atfsy419SDLYCaE30aJkRiiTCwGEY98vOhFsEVm7Zs4toQQ== dependencies: - "@typescript-eslint/experimental-utils" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" - integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== +"@typescript-eslint/experimental-utils@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz#bbe99a8d9510240c055fc4b17230dd0192ba3c7f" + integrity sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4183,14 +4183,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.8.0.tgz#e10f7c40c8cf2fb19920c879311e6c46ad17bacb" - integrity sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw== +"@typescript-eslint/parser@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.9.0.tgz#2e9cf438de119b143f642a3a406e1e27eb70b7cd" + integrity sha512-fJ+dNs3CCvEsJK2/Vg5c2ZjuQ860ySOAsodDPwBaVlrGvRN+iCNC8kUfLFL8cT49W4GSiLPa/bHiMjYXA7EhKQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.8.0" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4201,10 +4201,10 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" - integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== +"@typescript-eslint/typescript-estree@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz#09138daf8f47d0e494ba7db9e77394e928803017" + integrity sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From 3b6e51b2d8078180a0e6e7cfdadead4d6ae07ead Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 26 Nov 2019 19:56:52 +0100 Subject: [PATCH 51/52] [SIEM] Fix styled-components bump in siem app (#51559) --- .../components/edit_data_provider/index.tsx | 176 +++++++++--------- .../siem/public/components/loading/index.tsx | 46 ++--- .../siem/public/components/page/index.tsx | 16 +- .../column_headers/events_select/index.tsx | 4 +- .../timeline/data_providers/empty.tsx | 2 +- .../timeline/data_providers/providers.tsx | 2 +- .../components/timeline/properties/index.tsx | 39 ---- .../search_or_filter/search_or_filter.tsx | 88 ++++----- .../public/components/wrapper_page/index.tsx | 2 + 9 files changed, 177 insertions(+), 198 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index 214ac926e8868..18b271a3abc29 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -49,8 +49,7 @@ HeaderContainer.displayName = 'HeaderContainer'; // SIDE EFFECT: the following `createGlobalStyle` overrides the default styling // of euiComboBoxOptionsList because it's implemented as a popover, so it's // not selectable as a child of the styled component -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const StatefulEditDataProviderGlobalStyle = createGlobalStyle` .euiComboBoxOptionsList { z-index: 9999; } @@ -158,104 +157,107 @@ export const StatefulEditDataProvider = React.memo( }, []); return ( - - - - - - - 0 ? updatedField[0].label : null}> + <> + + + + + + + 0 ? updatedField[0].label : null}> + + + + + + + - - - + + + + + + + + + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - + - - - - - - + ) : null} - {updatedOperator.length > 0 && - updatedOperator[0].label !== i18n.EXISTS && - updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - - + - ) : null} - - - - - - - - { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} - size="s" - > - {i18n.SAVE} - - - - - - + + + + { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + }); + }} + size="s" + > + {i18n.SAVE} + + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx index eb85edce78a8f..42867c09b971b 100644 --- a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx @@ -10,8 +10,7 @@ import { pure } from 'recompose'; import styled, { createGlobalStyle } from 'styled-components'; // SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const LoadingPanelGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } @@ -41,27 +40,30 @@ export const LoadingPanel = pure( position = 'relative', zIndex = 'inherit', }) => ( - - - - - - - + <> + + + + + + + - - {text} - - - - - + + {text} + + + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index bc701006c3a9c..d56012de88929 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -15,9 +15,12 @@ import { } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; -// SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +/* + SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly + and `EuiPopover`, `EuiToolTip` global styles +*/ + +export const AppGlobalStyle = createGlobalStyle` div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -25,6 +28,13 @@ createGlobalStyle` div.application { background-color: rgba(0,0,0,0); } + + .euiPopover__panel.euiPopover__panel-isOpen { + z-index: 9900 !important; + } + .euiToolTip { + z-index: 9950 !important; + } `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index 747ef8f3ffe47..4f414af74a914 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -17,8 +17,7 @@ export const EVENTS_SELECT_WIDTH = 60; // px // SIDE EFFECT: the following `createGlobalStyle` overrides // the style of the select items -// eslint-disable-next-line -createGlobalStyle` +const EventsSelectGlobalStyle = createGlobalStyle` .eventsSelectItem { width: 100% !important; @@ -73,6 +72,7 @@ export const EventsSelect = pure(({ checkState, timelineId }) => { /> +
); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 29d2df5172457..3ef7240ee0375 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -50,7 +50,7 @@ const HighlightedBackground = styled.span` HighlightedBackground.displayName = 'HighlightedBackground'; const EmptyContainer = styled.div<{ showSmallMsg: boolean }>` - width: ${props => (props.showSmallMsg ? '60px' : 'auto')} + width: ${props => (props.showSmallMsg ? '60px' : 'auto')}; align-items: center; display: flex; flex-direction: row; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 112962367cd36..5a8654509fa88 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -46,7 +46,7 @@ interface Props { const ROW_OF_DATA_PROVIDERS_HEIGHT = 43; // px const PanelProviders = styled.div` - position: relative + position: relative; display: flex; flex-direction: row; min-height: 100px; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index ccc222673d7bc..7b69e006f48ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import styled, { createGlobalStyle } from 'styled-components'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -22,43 +20,6 @@ type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; -// SIDE EFFECT: the following `createGlobalStyle` overrides `EuiPopover` -// and `EuiToolTip` global styles: -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - } - .euiToolTip { - z-index: 9950 !important; - } -`; - -const Avatar = styled(EuiAvatar)` - margin-left: 5px; -`; - -Avatar.displayName = 'Avatar'; - -const DescriptionPopoverMenuContainer = styled.div` - margin-top: 15px; -`; - -DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; - -const SettingsIcon = styled(EuiIcon)` - margin-left: 4px; - cursor: pointer; -`; - -SettingsIcon.displayName = 'SettingsIcon'; - -const HiddenFlexItem = styled(EuiFlexItem)` - display: none; -`; - -HiddenFlexItem.displayName = 'HiddenFlexItem'; - interface Props { associateNote: AssociateNote; createTimeline: CreateTimeline; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 2d953ce3cfc95..eaa476bf3e2b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -26,8 +26,7 @@ const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; const searchOrFilterPopoverWidth = '352px'; // SIDE EFFECT: the following creates a global class selector -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const SearchOrFilterGlobalStyle = createGlobalStyle` .${timelineSelectModeItemsClassName} { width: 350px !important; } @@ -110,48 +109,51 @@ export const SearchOrFilter = pure( updateKqlMode, updateReduxTime, }) => ( - - - - - updateKqlMode({ id: timelineId, kqlMode: mode })} - options={options} - popoverClassName={searchOrFilterPopoverClassName} - valueOfSelected={kqlMode} + <> + + + + + updateKqlMode({ id: timelineId, kqlMode: mode })} + options={options} + popoverClassName={searchOrFilterPopoverClassName} + valueOfSelected={kqlMode} + /> + + + + - - - - - - - + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx index 5998aa527206e..309693427459e 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; +import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` ${({ theme }) => css` @@ -54,6 +55,7 @@ export const WrapperPage = React.memo( return ( {children} + ); } From f5296293c25504f280ce18c70f90f9e5e53e95c3 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 26 Nov 2019 14:22:02 -0500 Subject: [PATCH 52/52] [Canvas] Move workpad api routes to New Platform (#51116) * Move workpad api routes to New Platform * Cleanup * Clean up/Pr Feedback * Adding missing dependency to tests * Fix typecheck * Loosen workpad schema restrictions --- .../canvas/__tests__/fixtures/workpads.ts | 13 + .../canvas/public/lib/workpad_service.js | 1 + .../plugins/canvas/server/routes/index.ts | 2 - .../canvas/server/routes/workpad.test.js | 462 ------------------ .../plugins/canvas/server/routes/workpad.ts | 254 ---------- x-pack/plugins/canvas/kibana.json | 10 + x-pack/plugins/canvas/server/index.ts | 11 + x-pack/plugins/canvas/server/plugin.ts | 25 + .../server/routes/catch_error_handler.ts | 30 ++ x-pack/plugins/canvas/server/routes/index.ts | 17 + .../server/routes/workpad/create.test.ts | 102 ++++ .../canvas/server/routes/workpad/create.ts | 57 +++ .../server/routes/workpad/delete.test.ts | 78 +++ .../canvas/server/routes/workpad/delete.ts | 32 ++ .../canvas/server/routes/workpad/find.test.ts | 113 +++++ .../canvas/server/routes/workpad/find.ts | 60 +++ .../canvas/server/routes/workpad/get.test.ts | 140 ++++++ .../canvas/server/routes/workpad/get.ts | 65 +++ .../canvas/server/routes/workpad/index.ts | 21 + .../server/routes/workpad/ok_response.ts | 9 + .../server/routes/workpad/update.test.ts | 223 +++++++++ .../canvas/server/routes/workpad/update.ts | 129 +++++ .../server/routes/workpad/workpad_schema.ts | 65 +++ 23 files changed, 1201 insertions(+), 718 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.test.js delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.ts create mode 100644 x-pack/plugins/canvas/kibana.json create mode 100644 x-pack/plugins/canvas/server/index.ts create mode 100644 x-pack/plugins/canvas/server/plugin.ts create mode 100644 x-pack/plugins/canvas/server/routes/catch_error_handler.ts create mode 100644 x-pack/plugins/canvas/server/routes/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/ok_response.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts index d7ebbd87c97e6..271fc7a979057 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -192,3 +192,16 @@ export const elements: CanvasElement[] = [ { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, { ...BaseElement, expression: 'image | render' }, ]; + +export const workpadWithGroupAsElement: CanvasWorkpad = { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'image | render' }, + { ...BaseElement, id: 'group-1234' }, + ], + }, + ], +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index 33067f1837f41..f1ed069c15d4d 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -29,6 +29,7 @@ export function get(workpadId) { }); } +// TODO: I think this function is never used. Look into and remove the corresponding route as well export function update(id, workpad) { return fetch.put(`${apiPath}/${id}`, workpad); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index a0502c5e891a2..515d5b5e895ed 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { workpad } from './workpad'; import { esFields } from './es_fields'; import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; @@ -13,6 +12,5 @@ import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); - workpad(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js b/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js deleted file mode 100644 index 09a5c3b89c31e..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js +++ /dev/null @@ -1,462 +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 Hapi from 'hapi'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { workpad } from './workpad'; - -const routePrefix = API_ROUTE_WORKPAD; -const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; -const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - -jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); - -describe(`${CANVAS_TYPE} API`, () => { - const savedObjectsClient = { - get: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - }; - - afterEach(() => { - savedObjectsClient.get.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.find.mockReset(); - }); - - // Mock toISOString function of all Date types - global.Date = class Date extends global.Date { - toISOString() { - return '2019-02-12T21:01:22.479Z'; - } - }; - - // Setup mock server - const mockServer = new Hapi.Server({ debug: false, port: 0 }); - const mockEs = { - getCluster: () => ({ - errors: { - // formatResponse will fail without objects here - '400': Error, - '401': Error, - '403': Error, - '404': Error, - }, - }), - }; - - mockServer.ext('onRequest', (req, h) => { - req.getSavedObjectsClient = () => savedObjectsClient; - return h.continue; - }); - workpad(mockServer.route.bind(mockServer), mockEs); - - describe(`GET ${routePrefix}/{id}`, () => { - test('returns successful response', async () => { - const request = { - method: 'GET', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ id: '123', attributes: { foo: true } }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "foo": true, - "id": "123", -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - describe(`POST ${routePrefix}`, () => { - test('returns successful response without id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "workpad-123abc", - }, - ], -] -`); - }); - - test('returns succesful response with id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - id: '123', - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'PUT', - url: `${routePrefix}/123`, - payload: { - id: '234', - foo: true, - }, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`DELETE ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'DELETE', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.delete.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.delete.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - it(`GET ${routePrefix}/find`, async () => { - const request = { - method: 'GET', - url: `${routePrefix}/find?name=abc&page=2&perPage=10`, - }; - - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - attributes: { - foo: true, - }, - }, - ], - }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "workpads": Array [ - Object { - "foo": true, - "id": "1", - }, - ], -} -`); - expect(savedObjectsClient.find.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - Object { - "fields": Array [ - "id", - "name", - "@created", - "@timestamp", - ], - "page": "2", - "perPage": "10", - "search": "abc* | abc", - "searchFields": Array [ - "name", - ], - "sortField": "@timestamp", - "sortOrder": "desc", - "type": "canvas-workpad", - }, - ], -] -`); - }); - - describe(`PUT ${routePrefixAssets}/{id}`, () => { - test('only updates assets', async () => { - const request = { - method: 'PUT', - url: `${routePrefixAssets}/123`, - payload: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - 'asset-456': { - id: 'asset-456', - '@created': '2019-02-15T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }; - - // provide some existing workpad data to check that it's preserved - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - "asset-456": Object { - "@created": "2019-02-15T00:00:00.000Z", - "id": "asset-456", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "name": "fake workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefixStructures}/{id}`, () => { - test('only updates workpad', async () => { - const request = { - method: 'PUT', - url: `${routePrefixStructures}/123`, - payload: { - name: 'renamed workpad', - css: '.canvasPage { color: LavenderBlush; }', - }, - }; - - // provide some existing asset data and a name to replace - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - assets: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "css": ".canvasPage { color: LavenderBlush; }", - "name": "renamed workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts b/x-pack/legacy/plugins/canvas/server/routes/workpad.ts deleted file mode 100644 index 380fe97ca9ef1..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts +++ /dev/null @@ -1,254 +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 boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -import { CoreSetup } from '../shim'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CanvasWorkpad } from '../../types'; - -type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface WorkpadRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type WorkpadRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad; -}; - -type FindWorkpadRequest = WorkpadRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -type AssetsRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad['assets']; -}; - -export function workpad( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore EsErrors is not on the Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - const routePrefix = API_ROUTE_WORKPAD; - const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; - const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - const formatResponse = formatRes(esErrors); - - function createWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A workpad payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('workpad') } - ); - } - - function updateWorkpad( - req: WorkpadRequest | AssetsRequest, - newPayload?: CanvasWorkpad | { assets: CanvasWorkpad['assets'] } - ) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpadObject => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...(workpadObject.attributes as SavedObjectAttributes), - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - } - - function deleteWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CANVAS_TYPE, id); - } - - function findWorkpad(req: FindWorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CANVAS_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', '@created', '@timestamp'], - page, - perPage, - }); - } - - // get workpad - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient - .get(CANVAS_TYPE, id) - .then(obj => { - if ( - // not sure if we need to be this defensive - obj.type === 'canvas-workpad' && - obj.attributes && - obj.attributes.pages && - obj.attributes.pages.length - ) { - obj.attributes.pages.forEach(page => { - const elements = (page.elements || []).filter( - ({ id: pageId }) => !pageId.startsWith('group') - ); - const groups = (page.groups || []).concat( - (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) - ); - page.elements = elements; - page.groups = groups; - }); - } - return obj; - }) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse); - }, - }); - - // create workpad - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return createWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad assets - route({ - method: 'PUT', - path: `${routePrefixAssets}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: AssetsRequest) { - const payload = { assets: request.payload }; - return updateWorkpad(request, payload) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad structures - route({ - method: 'PUT', - path: `${routePrefixStructures}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // delete workpad - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler(request: WorkpadRequest) { - return deleteWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // find workpads - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler(request: FindWorkpadRequest) { - return findWorkpad(request) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - workpads: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - workpads: [], - }; - }); - }, - }); -} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json new file mode 100644 index 0000000000000..87214f0287054 --- /dev/null +++ b/x-pack/plugins/canvas/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "canvas", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "canvas"], + "server": true, + "ui": false, + "requiredPlugins": [] + } + \ No newline at end of file diff --git a/x-pack/plugins/canvas/server/index.ts b/x-pack/plugins/canvas/server/index.ts new file mode 100644 index 0000000000000..e881f7db69c78 --- /dev/null +++ b/x-pack/plugins/canvas/server/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { CanvasPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CanvasPlugin(initializerContext); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts new file mode 100644 index 0000000000000..76b86c2ac39b4 --- /dev/null +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -0,0 +1,25 @@ +/* + * 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 { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server'; +import { initRoutes } from './routes'; + +export class CanvasPlugin implements Plugin { + private readonly logger: Logger; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(coreSetup: CoreSetup): void { + const canvasRouter = coreSetup.http.createRouter(); + + initRoutes({ router: canvasRouter, logger: this.logger }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts new file mode 100644 index 0000000000000..fb7f4d6ee2600 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; + +export const catchErrorHandler: < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>( + fn: RequestHandler +) => RequestHandler = fn => { + return async (context, request, response) => { + try { + return await fn(context, request, response); + } catch (error) { + if (error.isBoom) { + return response.customError({ + body: error.output.payload, + statusCode: error.output.statusCode, + }); + } + return response.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts new file mode 100644 index 0000000000000..46873a6b32542 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, Logger } from 'src/core/server'; +import { initWorkpadRoutes } from './workpad'; + +export interface RouteInitializerDeps { + router: IRouter; + logger: Logger; +} + +export function initRoutes(deps: RouteInitializerDeps) { + initWorkpadRoutes(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts new file mode 100644 index 0000000000000..dbad1a97dc458 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -0,0 +1,102 @@ +/* + * 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 sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateWorkpadRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the workpad is created`, async () => { + const mockWorkpad = { + pages: [], + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: mockWorkpad, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...mockWorkpad, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `workpad-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts new file mode 100644 index 0000000000000..be904356720b6 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD}`, + validate: { + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + if (!request.body) { + return response.badRequest({ body: 'A workpad payload is required' }); + } + + const workpad = request.body as CanvasWorkpad; + + const now = new Date().toISOString(); + const { id, ...payload } = workpad; + + await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('workpad') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts new file mode 100644 index 0000000000000..e693840826b7a --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.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 { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteWorkpadRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the workpad is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith(CANVAS_TYPE, id); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts new file mode 100644 index 0000000000000..7adf11e7a887b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + context.core.savedObjects.client.delete(CANVAS_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts new file mode 100644 index 0000000000000..08de9b20e9818 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { initializeFindWorkpadsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindWorkpadsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found workpads`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 2, + "workpads": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 0, + "workpads": Array [], + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.ts b/x-pack/plugins/canvas/server/routes/workpad/find.ts new file mode 100644 index 0000000000000..a528a75611609 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.ts @@ -0,0 +1,60 @@ +/* + * 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'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindWorkpadsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const workpads = await savedObjectsClient.find({ + type: CANVAS_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: ['id', 'name', '@created', '@timestamp'], + page, + perPage, + }); + + return response.ok({ + body: { + total: workpads.total, + workpads: workpads.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + workpads: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts new file mode 100644 index 0000000000000..a31293f572c75 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetWorkpadRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpadWithGroupAsElement } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the workpad is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-workpad", + "123", + ], + ] + `); + }); + + it('corrects elements that should be groups', async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: workpadWithGroupAsElement as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + const workpad = response.payload as CanvasWorkpad; + + expect(response.status).toBe(200); + expect(workpad).not.toBeUndefined(); + + expect(workpad.pages[0].elements.length).toBe(1); + expect(workpad.pages[0].groups.length).toBe(1); + }); + + it('returns 404 if the workpad is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-workpad/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts new file mode 100644 index 0000000000000..7a51006aa9f02 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -0,0 +1,65 @@ +/* + * 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'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + request.params.id + ); + + if ( + // not sure if we need to be this defensive + workpad.type === 'canvas-workpad' && + workpad.attributes && + workpad.attributes.pages && + workpad.attributes.pages.length + ) { + workpad.attributes.pages.forEach(page => { + const elements = (page.elements || []).filter( + ({ id: pageId }) => !pageId.startsWith('group') + ); + const groups = (page.groups || []).concat( + (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) + ); + page.elements = elements; + page.groups = groups; + }); + } + + return response.ok({ + body: { + id: workpad.id, + ...workpad.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts new file mode 100644 index 0000000000000..8a61b30be5414 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeFindWorkpadsRoute } from './find'; +import { initializeGetWorkpadRoute } from './get'; +import { initializeCreateWorkpadRoute } from './create'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { initializeDeleteWorkpadRoute } from './delete'; + +export function initWorkpadRoutes(deps: RouteInitializerDeps) { + initializeFindWorkpadsRoute(deps); + initializeGetWorkpadRoute(deps); + initializeCreateWorkpadRoute(deps); + initializeUpdateWorkpadRoute(deps); + initializeUpdateWorkpadAssetsRoute(deps); + initializeDeleteWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts new file mode 100644 index 0000000000000..43d545a5183fe --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const okResponse = { + ok: true, +}; diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts new file mode 100644 index 0000000000000..492a6c98d71ee --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -0,0 +1,223 @@ +/* + * 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 sinon from 'sinon'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { okResponse } from './ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const workpad = workpads[0]; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('PUT workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the workpad is updated`, async () => { + const updatedWorkpad = { name: 'new name' }; + const { id, ...workpadAttributes } = workpad; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + body: updatedWorkpad, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: workpadAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...workpadAttributes, + ...updatedWorkpad, + '@timestamp': nowIso, + '@created': workpad['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing workpad is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CANVAS_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); + +describe('update assets', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadAssetsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it('updates assets', async () => { + const { id, ...attributes } = workpad; + const assets = { + 'asset-1': { + '@created': new Date().toISOString(), + id: 'asset-1', + type: 'asset', + value: 'some-url-encoded-asset', + }, + 'asset-2': { + '@created': new Date().toISOString(), + id: 'asset-2', + type: 'asset', + value: 'some-other asset', + }, + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad-assets/some-id', + params: { + id, + }, + body: assets, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: attributes as any, + references: [], + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...attributes, + '@timestamp': nowIso, + assets, + }, + { + id, + overwrite: true, + } + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts new file mode 100644 index 0000000000000..460aa174038ae --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.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 { schema, TypeOf } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { KibanaResponseFactory } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_STRUCTURES, + API_ROUTE_WORKPAD_ASSETS, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); + +const AssetPayloadSchema = schema.object({ + assets: AssetsRecordSchema, +}); + +const workpadUpdateHandler = async ( + payload: TypeOf | TypeOf, + id: string, + savedObjectsClient: SavedObjectsClientContract, + response: KibanaResponseFactory +) => { + const now = new Date().toISOString(); + + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( + CANVAS_TYPE, + { + ...workpadObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); +}; + +export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + // TODO: This route is likely deprecated and everything is using the workpad_structures + // path instead. Investigate further. + router.put( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); + + router.put( + { + path: `${API_ROUTE_WORKPAD_STRUCTURES}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); +} + +export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + + router.put( + { + path: `${API_ROUTE_WORKPAD_ASSETS}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + // ToDo: Currently the validation must be a schema.object + // Because we don't know what keys the assets will have, we have to allow + // unknowns and then validate in the handler + body: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, response) => { + return workpadUpdateHandler( + { assets: AssetsRecordSchema.validate(request.body) }, + request.params.id, + context.core.savedObjects.client, + response + ); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts new file mode 100644 index 0000000000000..0bcb161575901 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -0,0 +1,65 @@ +/* + * 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 PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const WorkpadElementSchema = schema.object({ + expression: schema.string(), + filter: schema.maybe(schema.nullable(schema.string())), + id: schema.string(), + position: PositionSchema, +}); + +export const WorkpadPageSchema = schema.object({ + elements: schema.arrayOf(WorkpadElementSchema), + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }) + ), + id: schema.string(), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const WorkpadAssetSchema = schema.object({ + '@created': schema.string(), + id: schema.string(), + type: schema.string(), + value: schema.string(), +}); + +export const WorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +});