From 067a810a4aa504e186b0664d8bcdf39f04606ecb Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 28 May 2020 15:56:16 -0500 Subject: [PATCH 01/38] [DOCS] Bumps up the Share dashboard page (#67696) --- docs/user/dashboard.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index 301efb2dfe2c0..1614f00f37ac7 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -160,7 +160,7 @@ When you're finished adding and arranging the panels, save the dashboard. . Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. [[sharing-dashboards]] -=== Share the dashboard +== Share the dashboard [[embedding-dashboards]] Share your dashboard outside of {kib}. From 7118e750a09169b8db7878c0ce0d420ef80fd68a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 28 May 2020 15:14:39 -0600 Subject: [PATCH 02/38] [Maps] allow adding multiple layers (#67544) * [Maps] allow adding multiple layers * update RenderWizardArguments arguments * fix toc_entry jest test * fix tslint error * cleanup * remove __transientLayerId from store signature * rename setSelectedLayerToFirstPreviewLayer * revert changes to es_search_source/create_source_editor.js --- .../maps/public/angular/map_controller.js | 2 - .../descriptor_types/descriptor_types.d.ts | 1 + .../maps/public/actions/layer_actions.ts | 64 ++++++++++++------- .../public/actions/map_action_constants.ts | 1 - .../maps/public/classes/layers/layer.tsx | 5 ++ .../classes/layers/layer_wizard_registry.ts | 2 +- .../observability_layer_template.tsx | 13 ++-- .../upload_layer_wizard.tsx | 10 +-- .../ems_boundaries_layer_wizard.tsx | 4 +- .../ems_base_map_layer_wizard.tsx | 4 +- .../clusters_layer_wizard.tsx | 6 +- .../heatmap_layer_wizard.tsx | 6 +- .../point_2_point_layer_wizard.tsx | 6 +- .../es_documents_layer_wizard.tsx | 6 +- .../kibana_regionmap_layer_wizard.tsx | 4 +- .../kibana_base_map_layer_wizard.tsx | 4 +- .../layer_wizard.tsx | 4 +- .../sources/wms_source/wms_layer_wizard.tsx | 6 +- .../sources/xyz_tms_source/layer_wizard.tsx | 4 +- .../flyout_body/flyout_body.tsx | 2 +- .../add_layer_panel/flyout_footer/index.ts | 16 +++-- .../add_layer_panel/flyout_footer/view.tsx | 8 +-- .../add_layer_panel/index.ts | 24 +++---- .../add_layer_panel/view.tsx | 30 +++------ .../toc_entry/__snapshots__/view.test.js.snap | 5 ++ .../layer_toc/toc_entry/index.js | 2 - .../layer_control/layer_toc/toc_entry/view.js | 3 +- .../layer_toc/toc_entry/view.test.js | 3 + x-pack/plugins/maps/public/reducers/map.d.ts | 1 - x-pack/plugins/maps/public/reducers/map.js | 5 -- .../maps/public/selectors/map_selectors.ts | 60 +++++++++-------- 31 files changed, 161 insertions(+), 150 deletions(-) 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 91b54d2698c1d..70d5195feef42 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -38,7 +38,6 @@ import { setGotoWithCenter, replaceLayerList, setQuery, - clearTransientLayerStateAndCloseFlyout, setMapSettings, enableFullScreen, updateFlyout, @@ -535,7 +534,6 @@ app.controller( addHelpMenuToAppChrome(); async function doSave(saveOptions) { - await store.dispatch(clearTransientLayerStateAndCloseFlyout()); savedMap.syncWithStore(store.getState()); let id; diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index 4bdafcabaad06..b412375874f68 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -132,6 +132,7 @@ export type SourceDescriptor = export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; __isInErrorState?: boolean; + __isPreviewLayer?: boolean; __errorMessage?: string; __trackedLayerDescriptor?: LayerDescriptor; alpha?: number; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index cac79093ce437..51e251a5d8e20 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -9,10 +9,10 @@ import { Query } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { getLayerById, + getLayerList, getLayerListRaw, getSelectedLayerId, getMapReady, - getTransientLayerId, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; import { cancelRequest } from '../reducers/non_serializable_instances'; @@ -27,7 +27,6 @@ import { SET_JOINS, SET_LAYER_VISIBILITY, SET_SELECTED_LAYER, - SET_TRANSIENT_LAYER, SET_WAITING_FOR_READY_HIDDEN_LAYERS, TRACK_CURRENT_LAYER_STATE, UPDATE_LAYER_ORDER, @@ -139,6 +138,41 @@ export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) { }; } +export function addPreviewLayers(layerDescriptors: LayerDescriptor[]) { + return (dispatch: Dispatch) => { + dispatch(removePreviewLayers()); + + layerDescriptors.forEach((layerDescriptor) => { + dispatch(addLayer({ ...layerDescriptor, __isPreviewLayer: true })); + }); + }; +} + +export function removePreviewLayers() { + return (dispatch: Dispatch, getState: () => MapStoreState) => { + getLayerList(getState()).forEach((layer) => { + if (layer.isPreviewLayer()) { + dispatch(removeLayer(layer.getId())); + } + }); + }; +} + +export function promotePreviewLayers() { + return (dispatch: Dispatch, getState: () => MapStoreState) => { + getLayerList(getState()).forEach((layer) => { + if (layer.isPreviewLayer()) { + dispatch({ + type: UPDATE_LAYER_PROP, + id: layer.getId(), + propName: '__isPreviewLayer', + newValue: false, + }); + } + }); + }; +} + export function setLayerVisibility(layerId: string, makeVisible: boolean) { return async (dispatch: Dispatch, getState: () => MapStoreState) => { // if the current-state is invisible, we also want to sync data @@ -193,31 +227,17 @@ export function setSelectedLayer(layerId: string | null) { }; } -export function removeTransientLayer() { +export function setFirstPreviewLayerToSelectedLayer() { return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const transientLayerId = getTransientLayerId(getState()); - if (transientLayerId) { - await dispatch(removeLayerFromLayerList(transientLayerId)); - await dispatch(setTransientLayer(null)); + const firstPreviewLayer = getLayerList(getState()).find((layer) => { + return layer.isPreviewLayer(); + }); + if (firstPreviewLayer) { + dispatch(setSelectedLayer(firstPreviewLayer.getId())); } }; } -export function setTransientLayer(layerId: string | null) { - return { - type: SET_TRANSIENT_LAYER, - transientLayerId: layerId, - }; -} - -export function clearTransientLayerStateAndCloseFlyout() { - return async (dispatch: Dispatch) => { - await dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - }; -} - export function updateLayerOrder(newLayerOrder: number[]) { return { type: UPDATE_LAYER_ORDER, diff --git a/x-pack/plugins/maps/public/actions/map_action_constants.ts b/x-pack/plugins/maps/public/actions/map_action_constants.ts index 0a32dba119429..25a86e4c50d07 100644 --- a/x-pack/plugins/maps/public/actions/map_action_constants.ts +++ b/x-pack/plugins/maps/public/actions/map_action_constants.ts @@ -5,7 +5,6 @@ */ export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER'; -export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER'; export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER'; export const ADD_LAYER = 'ADD_LAYER'; export const SET_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS'; diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 263e9888cd059..5d54166e08fb7 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -80,6 +80,7 @@ export interface ILayer { getInFlightRequestTokens(): symbol[]; getPrevRequestToken(dataId: string): symbol | undefined; destroy: () => void; + isPreviewLayer: () => boolean; } export type Footnote = { icon: ReactElement; @@ -179,6 +180,10 @@ export class AbstractLayer implements ILayer { return this.getSource().isJoinable(); } + isPreviewLayer(): boolean { + return !!this._descriptor.__isPreviewLayer; + } + supportsElasticsearchFilters(): boolean { return this.getSource().isESSource(); } diff --git a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts index 7698fb7c0947e..2bdeb6446cf28 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts @@ -9,7 +9,7 @@ import { ReactElement } from 'react'; import { LayerDescriptor } from '../../../common/descriptor_types'; export type RenderWizardArguments = { - previewLayer: (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => void; + previewLayers: (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => void; mapColors: string[]; // upload arguments isIndexingTriggered: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx index bfd78d5490059..3f3c556dcae1e 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx @@ -53,13 +53,12 @@ export class ObservabilityLayerTemplate extends Component { function previewGeojsonFile(geojsonFile: unknown, name: string) { if (!geojsonFile) { - previewLayer(null); + previewLayers([]); return; } const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); // TODO figure out a better way to handle passing this information back to layer_addpanel - previewLayer(layerDescriptor, true); + previewLayers([layerDescriptor], true); } function viewIndexedData(indexResponses: { @@ -72,7 +72,7 @@ export const uploadLayerWizardConfig: LayerWizard = { ) ); if (!indexPatternId || !geoField) { - previewLayer(null); + previewLayers([]); } else { const esSearchSourceConfig = { indexPatternId, @@ -85,7 +85,7 @@ export const uploadLayerWizardConfig: LayerWizard = { ? SCALING_TYPES.CLUSTERS : SCALING_TYPES.LIMIT, }; - previewLayer(createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)); + previewLayers([createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)]); importSuccessHandler(indexResponses); } } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index 4f1edca75b308..7eec84ef5bb2e 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -22,11 +22,11 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), icon: 'emsApp', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 7a25609c6a5d1..60e67b1ae7053 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -22,12 +22,12 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { defaultMessage: 'Tile map service from Elastic Maps Service', }), icon: 'emsApp', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { const layerDescriptor = VectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 84bdee2a64bd8..b9d5faa8e18f1 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -34,10 +34,10 @@ export const clustersLayerWizardConfig: LayerWizard = { defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } @@ -93,7 +93,7 @@ export const clustersLayerWizardConfig: LayerWizard = { }, }), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ( diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index d0e45cb05ca06..79252c7febf8c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -21,17 +21,17 @@ export const heatmapLayerWizardConfig: LayerWizard = { defaultMessage: 'Geospatial data grouped in grids to show density', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } const layerDescriptor = HeatmapLayer.createDescriptor({ sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ( diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index 8d7bf0d2af661..5169af9bdddf2 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -28,10 +28,10 @@ export const point2PointLayerWizardConfig: LayerWizard = { defaultMessage: 'Aggregated data paths between the source and destination', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } @@ -64,7 +64,7 @@ export const point2PointLayerWizardConfig: LayerWizard = { }, }), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 8898735427ccb..888de2e7297cb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -28,14 +28,14 @@ export const esDocumentsLayerWizardConfig: LayerWizard = { defaultMessage: 'Vector data from a Kibana index pattern', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } - previewLayer(createDefaultLayerDescriptor(sourceConfig, mapColors)); + previewLayers([createDefaultLayerDescriptor(sourceConfig, mapColors)]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 309cb3abd83b2..b778dc0076459 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -24,11 +24,11 @@ export const kibanaRegionMapLayerWizardConfig: LayerWizard = { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', }), icon: 'logoKibana', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index 46513985ed1ab..227c0182b98de 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -24,12 +24,12 @@ export const kibanaBasemapLayerWizardConfig: LayerWizard = { defaultMessage: 'Tile map service configured in kibana.yml', }), icon: 'logoKibana', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = () => { const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: KibanaTilemapSource.createDescriptor(), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index 86f8108d5e23b..c29302a2058b2 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -19,11 +19,11 @@ export const mvtVectorSourceWizardConfig: LayerWizard = { defaultMessage: 'Vector source wizard', }), icon: 'grid', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index 9261b8866d115..62eeef234f414 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -18,17 +18,17 @@ export const wmsLayerWizardConfig: LayerWizard = { defaultMessage: 'Maps from OGC Standard WMS', }), icon: 'grid', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: WMSSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index 574aaa262569f..b99b17c1d22d4 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -16,12 +16,12 @@ export const tmsLayerWizardConfig: LayerWizard = { defaultMessage: 'Tile map service configured in interface', }), icon: 'grid', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx index 75fb7a5bc4acc..b287064938ce5 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx @@ -24,7 +24,7 @@ export const FlyoutBody = (props: Props) => { } const renderWizardArgs = { - previewLayer: props.previewLayer, + previewLayers: props.previewLayers, mapColors: props.mapColors, isIndexingTriggered: props.isIndexingTriggered, onRemove: props.onRemove, diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts index 968429ce91226..470e83f2d8090 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts @@ -7,22 +7,24 @@ import { AnyAction, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { FlyoutFooter } from './view'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { clearTransientLayerStateAndCloseFlyout } from '../../../actions'; +import { hasPreviewLayers, isLoadingPreviewLayers } from '../../../selectors/map_selectors'; +import { removePreviewLayers, updateFlyout } from '../../../actions'; import { MapStoreState } from '../../../reducers/store'; +import { FLYOUT_STATE } from '../../../reducers/ui'; function mapStateToProps(state: MapStoreState) { - const selectedLayer = getSelectedLayer(state); - const hasLayerSelected = !!selectedLayer; return { - hasLayerSelected, - isLoading: hasLayerSelected && selectedLayer!.isLayerLoading(), + hasPreviewLayers: hasPreviewLayers(state), + isLoading: isLoadingPreviewLayers(state), }; } function mapDispatchToProps(dispatch: Dispatch) { return { - closeFlyout: () => dispatch(clearTransientLayerStateAndCloseFlyout()), + closeFlyout: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx index 6f4d25a9c6c3e..2e122324c50fb 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx @@ -20,7 +20,7 @@ interface Props { disableNextButton: boolean; nextButtonText: string; closeFlyout: () => void; - hasLayerSelected: boolean; + hasPreviewLayers: boolean; isLoading: boolean; } @@ -30,14 +30,14 @@ export const FlyoutFooter = ({ disableNextButton, nextButtonText, closeFlyout, - hasLayerSelected, + hasPreviewLayers, isLoading, }: Props) => { const nextButton = showNextButton ? ( ) { return { - previewLayer: async (layerDescriptor: LayerDescriptor) => { - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - dispatch(addLayer(layerDescriptor)); - dispatch(setSelectedLayer(layerDescriptor.id)); - dispatch(setTransientLayer(layerDescriptor.id)); + addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => { + dispatch(addPreviewLayers(layerDescriptors)); }, - removeTransientLayer: () => { - dispatch(setSelectedLayer(null)); - dispatch(removeTransientLayer()); - }, - selectLayerAndAdd: () => { - dispatch(setTransientLayer(null)); + promotePreviewLayers: () => { + dispatch(setFirstPreviewLayerToSelectedLayer()); dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); + dispatch(promotePreviewLayers()); }, setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), resetIndexing: () => dispatch(updateIndexingStage(null)), diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx index d382a4085fe19..c1b6dcc1e12a6 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx @@ -17,17 +17,15 @@ interface Props { isIndexingReady: boolean; isIndexingSuccess: boolean; isIndexingTriggered: boolean; - previewLayer: (layerDescriptor: LayerDescriptor) => void; - removeTransientLayer: () => void; + addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => void; + promotePreviewLayers: () => void; resetIndexing: () => void; - selectLayerAndAdd: () => void; setIndexingTriggered: () => void; } interface State { importView: boolean; isIndexingSource: boolean; - layerDescriptor: LayerDescriptor | null; layerImportAddReady: boolean; layerWizard: LayerWizard | null; } @@ -37,7 +35,6 @@ export class AddLayerPanel extends Component { state = { layerWizard: null, - layerDescriptor: null, // TODO get this from redux store instead of storing locally isIndexingSource: false, importView: false, layerImportAddReady: false, @@ -57,21 +54,13 @@ export class AddLayerPanel extends Component { } } - _previewLayer = (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => { + _previewLayers = (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => { if (!this._isMounted) { return; } - if (!layerDescriptor) { - this.setState({ - layerDescriptor: null, - isIndexingSource: false, - }); - this.props.removeTransientLayer(); - return; - } - this.setState({ layerDescriptor, isIndexingSource: !!isIndexingSource }); - this.props.previewLayer(layerDescriptor); + this.setState({ isIndexingSource: layerDescriptors.length ? !!isIndexingSource : false }); + this.props.addPreviewLayers(layerDescriptors); }; _clearLayerData = ({ keepSourceType = false }: { keepSourceType: boolean }) => { @@ -80,7 +69,6 @@ export class AddLayerPanel extends Component { } const newState: Partial = { - layerDescriptor: null, isIndexingSource: false, }; if (!keepSourceType) { @@ -90,7 +78,7 @@ export class AddLayerPanel extends Component { // @ts-ignore this.setState(newState); - this.props.removeTransientLayer(); + this.props.addPreviewLayers([]); }; _onWizardSelect = (layerWizard: LayerWizard) => { @@ -101,7 +89,7 @@ export class AddLayerPanel extends Component { if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { this.props.setIndexingTriggered(); } else { - this.props.selectLayerAndAdd(); + this.props.promotePreviewLayers(); if (this.state.importView) { this.setState({ layerImportAddReady: false, @@ -126,7 +114,7 @@ export class AddLayerPanel extends Component { }); const isNextBtnEnabled = this.state.importView ? this.props.isIndexingReady || this.props.isIndexingSuccess - : !!this.state.layerDescriptor; + : true; return ( @@ -141,7 +129,7 @@ export class AddLayerPanel extends Component { onClear={() => this._clearLayerData({ keepSourceType: false })} onRemove={() => this._clearLayerData({ keepSourceType: true })} onWizardSelect={this._onWizardSelect} - previewLayer={this._previewLayer} + previewLayers={this._previewLayers} /> { - await dispatch(removeTransientLayer()); await dispatch(setSelectedLayer(layerId)); dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); }, diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index c0ce24fef9cd8..b17078ae37113 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -239,7 +239,8 @@ export class TOCEntry extends React.Component { 'mapTocEntry-isDragging': this.props.isDragging, 'mapTocEntry-isDraggingOver': this.props.isDraggingOver, 'mapTocEntry-isSelected': - this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId(), + this.props.layer.isPreviewLayer() || + (this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId()), }); return ( diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js index 90d756484c47f..543be9395d0bc 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js @@ -21,6 +21,9 @@ const mockLayer = { getDisplayName: () => { return 'layer 1'; }, + isPreviewLayer: () => { + return false; + }, isVisible: () => { return true; }, diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 8fc655b2c837a..33794fcf8657d 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -66,7 +66,6 @@ export type MapState = { openTooltips: TooltipState[]; mapState: MapContext; selectedLayerId: string | null; - __transientLayerId: string | null; layerList: LayerDescriptor[]; waitingForMapReadyLayerList: LayerDescriptor[]; settings: MapSettings; diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index c5f3968b749f1..9a661fe4833a8 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -6,7 +6,6 @@ import { SET_SELECTED_LAYER, - SET_TRANSIENT_LAYER, UPDATE_LAYER_ORDER, LAYER_DATA_LOAD_STARTED, LAYER_DATA_LOAD_ENDED, @@ -126,7 +125,6 @@ export const DEFAULT_MAP_STATE = { hideViewControl: false, }, selectedLayerId: null, - __transientLayerId: null, layerList: [], waitingForMapReadyLayerList: [], settings: getDefaultMapSettings(), @@ -285,9 +283,6 @@ export function map(state = DEFAULT_MAP_STATE, action) { case SET_SELECTED_LAYER: const selectedMatch = state.layerList.find((layer) => layer.id === action.selectedLayerId); return { ...state, selectedLayerId: selectedMatch ? action.selectedLayerId : null }; - case SET_TRANSIENT_LAYER: - const transientMatch = state.layerList.find((layer) => layer.id === action.transientLayerId); - return { ...state, __transientLayerId: transientMatch ? action.transientLayerId : null }; case UPDATE_LAYER_ORDER: return { ...state, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 0789222b0bf38..fd887d360c2e0 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -137,9 +137,6 @@ export const getSelectedLayerId = ({ map }: MapStoreState): string | null => { return !map.selectedLayerId || !map.layerList ? null : map.selectedLayerId; }; -export const getTransientLayerId = ({ map }: MapStoreState): string | null => - map.__transientLayerId; - export const getLayerListRaw = ({ map }: MapStoreState): LayerDescriptor[] => map.layerList ? map.layerList : []; @@ -331,15 +328,28 @@ export const getSelectedLayer = createSelector( } ); -export const getMapColors = createSelector( - getTransientLayerId, - getLayerListRaw, - (transientLayerId, layerList) => - layerList.reduce((accu: string[], layer: LayerDescriptor) => { - if (layer.id === transientLayerId) { - return accu; - } - const color: string | undefined = _.get(layer, 'style.properties.fillColor.options.color'); +export const hasPreviewLayers = createSelector(getLayerList, (layerList) => { + return layerList.some((layer) => { + return layer.isPreviewLayer(); + }); +}); + +export const isLoadingPreviewLayers = createSelector(getLayerList, (layerList) => { + return layerList.some((layer) => { + return layer.isPreviewLayer() && layer.isLayerLoading(); + }); +}); + +export const getMapColors = createSelector(getLayerListRaw, (layerList) => + layerList + .filter((layerDescriptor) => { + return !layerDescriptor.__isPreviewLayer; + }) + .reduce((accu: string[], layerDescriptor: LayerDescriptor) => { + const color: string | undefined = _.get( + layerDescriptor, + 'style.properties.fillColor.options.color' + ); if (color) accu.push(color); return accu; }, []) @@ -373,24 +383,20 @@ export const getQueryableUniqueIndexPatternIds = createSelector(getLayerList, (l return _.uniq(indexPatternIds); }); -export const hasDirtyState = createSelector( - getLayerListRaw, - getTransientLayerId, - (layerListRaw, transientLayerId) => { - if (transientLayerId) { +export const hasDirtyState = createSelector(getLayerListRaw, (layerListRaw) => { + return layerListRaw.some((layerDescriptor) => { + if (layerDescriptor.__isPreviewLayer) { return true; } - return layerListRaw.some((layerDescriptor) => { - const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR]; - if (!trackedState) { - return false; - } - const currentState = copyPersistentState(layerDescriptor); - return !_.isEqual(currentState, trackedState); - }); - } -); + const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR]; + if (!trackedState) { + return false; + } + const currentState = copyPersistentState(layerDescriptor); + return !_.isEqual(currentState, trackedState); + }); +}); export const areLayersLoaded = createSelector( getLayerList, From 05675602ee6f275f781e7cb328227f253dbfef98 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 28 May 2020 16:39:16 -0500 Subject: [PATCH 03/38] [DOCS] Updates to Lens docs (#67694) * [DOCS] Updates to Lens docs * Fixed image * Update docs/visualize/lens.asciidoc Co-authored-by: Wylie Conlon * Update docs/visualize/lens.asciidoc Co-authored-by: Wylie Conlon * Update docs/visualize/lens.asciidoc Co-authored-by: Wylie Conlon * Update docs/visualize/lens.asciidoc Co-authored-by: Wylie Conlon * Update docs/visualize/lens.asciidoc Co-authored-by: Wylie Conlon * Update docs/visualize/lens.asciidoc Co-authored-by: Wylie Conlon * Comment from Wylie Co-authored-by: Wylie Conlon --- docs/images/lens_viz_types.png | Bin 0 -> 27052 bytes docs/visualize/lens.asciidoc | 127 ++++++++++++++------------------- 2 files changed, 55 insertions(+), 72 deletions(-) create mode 100644 docs/images/lens_viz_types.png diff --git a/docs/images/lens_viz_types.png b/docs/images/lens_viz_types.png new file mode 100644 index 0000000000000000000000000000000000000000..fb3961ad8bb283b5581795a280b05ad7c6dcaba2 GIT binary patch literal 27052 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 you have defined in -{kib}, and the current time range. When you change the index pattern or time filter, -the list of fields are updated. +The fields in the data panel based on your selected <>, and the <>. -To narrow the list of fields, you can: +To change the index pattern, click it, then select a new one. The fields in the data panel automatically update. -* Enter the field name in *Search field names*. +To filter the fields in the data panel: -* Click *Filter by type*, then select the filter. You can also select *Only show fields with data* -to show the full list of fields from the index pattern. +* Enter the name in *Search field names*. + +* Click *Filter by type*, then select the filter. To show all of the fields in the index pattern, deselect *Only show fields with data*. [float] [[view-data-summaries]] ==== Data summaries -To help you decide exactly the data you want to display, get a quick summary of each data field. -The summary shows the distribution of values in the time range. +To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of values in the time range. -To view the data information, navigate to a data field, then click *i*. +To view the field summary information, navigate to the field, then click *i*. [role="screenshot"] image::images/lens_data_info.png[] @@ -66,46 +57,40 @@ image::images/lens_data_info.png[] [[change-the-visualization-type]] ==== Change the visualization type -With Lens, you are no longer required to build each visualization from scratch. Lens allows -you to switch between any supported chart type at any time. Lens also provides -suggestions, which are shortcuts to alternate visualizations based on the data you have. +*Lens* enables you to switch between any supported visualization type at any time. -You can switch between suggestions without losing your previous state: +*Suggestions* are shortcuts to alternate visualizations that *Lens* generates for you. [role="screenshot"] image::images/lens_suggestions.gif[] -If you want to switch to a chart type that is not suggested, click the chart type, -then select a chart type. When there is an exclamation point (!) -next to a chart type, Lens is unable to transfer your current data, but +If you'd like to use a visualization type that is not suggested, click the visualization type, +then select a new one. + +[role="screenshot"] +image::images/lens_viz_types.png[] + +When there is an exclamation point (!) +next to a visualization type, Lens is unable to transfer your data, but still allows you to make the change. [float] [[customize-operation]] -==== Customize the data for your visualization +==== Change the aggregation and labels Lens allows some customizations of the data for each visualization. -. Click the index pattern name, then select the new index pattern. -+ -If there is a match, Lens displays the new data. All fields that do not match the index pattern are removed. - -. Change the data field options, such as the aggregation or label. - -.. Click *Drop a field here* or the field name in the column. +. Click *Drop a field here* or the field name in the column. -.. Change the options that appear depending on the type of field. +. Change the options that appear depending on the type of field. [float] [[layers]] -==== Layers in bar, line, and area charts +==== Add layers and indices -The bar, line, and area charts allow you to layer two different series. To add a layer, click *+*. +Bar, line, and area charts allow you to visualize multiple data layers and indices so that you can compare and analyze data from multiple sources. -To remove a layer, click the chart icon next to the index name: - -[role="screenshot"] -image::images/lens_remove_layer.png[] +To add a layer, click *+*, then drag and drop the fields for the new layer. To view a different index, click it, then select a new one. [float] [[lens-tutorial]] @@ -125,50 +110,48 @@ To start, you'll need to add the <>. Drag and drop your data onto the visualization builder pane. -. Open *Visualize*, then click *Create visualization*. +. From the menu, click *Visualize*, then click *Create visualization*. . On the *New Visualization* window, click *Lens*. -. Select the *kibana_sample_data_ecommerce* index. +. Select the *kibana_sample_data_ecommerce* index pattern. -. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. The list of data fields are updated. +. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. ++ +The fields in the data panel update. . Drag and drop the *taxful_total_price* data field to the visualization builder pane. + [role="screenshot"] image::images/lens_tutorial_1.png[Lens tutorial] -Lens has taken your intent to see *taxful_total_price* and added in the *order_date* field to show -average order prices over time. +To display the average order prices over time, *Lens* automatically added in *order_date* field. To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens -understands that you want to show the top categories and compare them across the dates, -and creates a chart that compares the sales for each of the top 3 categories: +knows that you want to show the top categories and compare them across the dates, +and creates a chart that compares the sales for each of the top three categories: [role="screenshot"] image::images/lens_tutorial_2.png[Lens tutorial] [float] [[customize-lens-visualization]] -==== Further customization +==== Customize your visualization -Customize your visualization to look exactly how you want. +Make your visualization look exactly how you want with the customization options. . Click *Average of taxful_total_price*. -.. Change the *Label* to `Sales`, or a name that you prefer for the data. +.. Change the *Label* to `Sales`. . Click *Top values of category.keyword*. -.. Increase *Number of values* to `10`. The visualization updates in the background to show there are only +.. Change *Number of values* to `10`. The visualization updates to show there are only six available categories. ++ +Look at the *Suggestions*. An area chart is not an option, but for sales data, a stacked area chart might be the best option. -. Look at the suggestions. None of them show an area chart, but for sales data, a stacked area chart -might make sense. To switch the chart type: - -.. Click *Stacked bar chart* in the column. - -.. Click *Stacked area*. +. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. + [role="screenshot"] image::images/lens_tutorial_3.png[Lens tutorial] @@ -177,6 +160,6 @@ image::images/lens_tutorial_3.png[Lens tutorial] [[lens-tutorial-next-steps]] ==== Next steps -Now that you've created your visualization in Lens, you can add it to a Dashboard. +Now that you've created your visualization, you can add it to a dashboard or Canvas workpad. -For more information, see <>. +For more information, refer to <> or <>. From 7f2e32475af8aa46877e96754b26f0d752ad8977 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 28 May 2020 18:25:52 -0400 Subject: [PATCH 04/38] [CI] Add new intake worker size with 2x memory, and move workspace to memory (#67676) Co-authored-by: Elastic Machine --- vars/workers.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vars/workers.groovy b/vars/workers.groovy index d2cc19787bc5f..387f62a625230 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -5,6 +5,8 @@ def label(size) { switch(size) { case 's': return 'linux && immutable' + case 's-highmem': + return 'tests-s' case 'l': return 'tests-l' case 'xl': @@ -114,7 +116,7 @@ def ci(Map params, Closure closure) { // Worker for running the current intake jobs. Just runs a single script after bootstrap. def intake(jobName, String script) { return { - ci(name: jobName, size: 's', ramDisk: false) { + ci(name: jobName, size: 's-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { runbld(script, "Execute ${jobName}") } From 957915b7e5a9487f9cd8ff4fb20ad9ea0a13742a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 28 May 2020 16:45:29 -0600 Subject: [PATCH 05/38] [SIEM][Lists] Adds circular dependency checker for lists plugin ## Summary * Added dependency checker for the public and common folders for lists --- test/scripts/jenkins_xpack.sh | 6 +++ .../lists/scripts/check_circular_deps.js | 8 ++++ .../run_check_circular_deps_cli.ts | 45 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 x-pack/plugins/lists/scripts/check_circular_deps.js create mode 100644 x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index ebc58c5e4e773..50a92a41e3932 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -21,6 +21,12 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo "" echo "" + echo " -> Running List cyclic dependency test" + cd "$XPACK_DIR" + checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps + echo "" + echo "" + # echo " -> Running jest integration tests" # cd "$XPACK_DIR" # node scripts/jest_integration --ci --verbose diff --git a/x-pack/plugins/lists/scripts/check_circular_deps.js b/x-pack/plugins/lists/scripts/check_circular_deps.js new file mode 100644 index 0000000000000..4ba7020d13465 --- /dev/null +++ b/x-pack/plugins/lists/scripts/check_circular_deps.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +require('../../../../src/setup_node_env'); +require('./check_circular_deps/run_check_circular_deps_cli'); diff --git a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts new file mode 100644 index 0000000000000..430e4983882cb --- /dev/null +++ b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; + +// @ts-ignore +import madge from 'madge'; +import { createFailError, run } from '@kbn/dev-utils'; + +run( + async ({ log }) => { + const result = await madge( + [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], + { + excludeRegExp: [ + 'test.ts$', + 'test.tsx$', + 'src/core/server/types.ts$', + 'src/core/server/saved_objects/types.ts$', + 'src/core/public/chrome/chrome_service.tsx$', + 'src/core/public/overlays/banners/banners_service.tsx$', + 'src/core/public/saved_objects/saved_objects_client.ts$', + 'src/plugins/data/public', + 'src/plugins/ui_actions/public', + ], + fileExtensions: ['ts', 'js', 'tsx'], + } + ); + + const circularFound = result.circular(); + if (circularFound.length !== 0) { + throw createFailError( + `Lists circular dependencies of imports has been found:\n - ${circularFound.join('\n - ')}` + ); + } else { + log.success('No circular deps 👍'); + } + }, + { + description: 'Check the Lists plugin for circular deps', + } +); From 92d5fcdc1c13f9e353bad8d60484ce9c68281fbe Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 28 May 2020 17:46:43 -0500 Subject: [PATCH 06/38] [APM] Ensure loading indicator stops in Safari (#67695) The combination of using object destructuring and numeric object keys in the reducer for LoadingIndicatorContext caused it so the loading indicator would not disappear in 7.8 in Safari even though there were no more loading statuses. Optimization changes between 7.8 and master may be why this is only appears on 7.8. Update this reducer to stringify the key and `lodash.pick` only the true values so the only pairs in the object are ones with `true` as the value. Fixes #67334. --- .../public/context/LoadingIndicatorContext.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx index c1776d7437e05..32e52f8e396b5 100644 --- a/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx +++ b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiPortal, EuiProgress } from '@elastic/eui'; +import { pick } from 'lodash'; import React, { Fragment, useMemo, useReducer } from 'react'; import { useDelayedVisibility } from '../components/shared/useDelayedVisibility'; export const LoadingIndicatorContext = React.createContext({ statuses: {}, - dispatchStatus: (action: Action) => undefined as void, + dispatchStatus: (action: Action) => {}, }); interface State { @@ -22,14 +23,13 @@ interface Action { } function reducer(statuses: State, action: Action) { - // add loading status - if (action.isLoading) { - return { ...statuses, [action.id]: true }; - } - - // remove loading status - const { [action.id]: statusToRemove, ...restStatuses } = statuses; - return restStatuses; + // Return an object with only the ids with `true` as their value, so that ids + // that previously had `false` are removed and do not remain hanging around in + // the object. + return pick( + { ...statuses, [action.id.toString()]: action.isLoading }, + Boolean + ); } function getIsAnyLoading(statuses: State) { From 043ecaca1a170e29b54ff93893f3edd4a3af561e Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 28 May 2020 19:59:32 -0400 Subject: [PATCH 07/38] [SECURITY] bug 667 (#67674) * bug 667 * update snapshot --- .../__snapshots__/timeline.test.tsx.snap | 2 +- .../components/timeline/helpers.test.tsx | 19 ++++++++++++++++++- .../timelines/components/timeline/helpers.tsx | 6 +++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 3854fc6b985ac..4ed0b52fc0f14 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/siem/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -843,7 +843,7 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` "message", ] } - filterQuery="{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}}]}}]}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 6\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 7\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 8\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 9\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 10\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":1521830963132}}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"lte\\":1521862432253}}}],\\"minimum_should_match\\":1}}]}}]}}],\\"should\\":[],\\"must_not\\":[]}}" + filterQuery="{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}}]}}]}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 6\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 7\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 8\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 9\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 10\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":1521830963132}}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"lte\\":1521862432253}}}],\\"minimum_should_match\\":1}}]}}]}}],\\"should\\":[],\\"must_not\\":[]}}" id="foo" indexToAdd={Array []} limit={5} diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx index 87eb9cc45b98b..ede8d7cfded58 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx @@ -58,7 +58,7 @@ describe('Build KQL Query', () => { test('Build KQL query with two data provider', () => { const dataProviders = mockDataProviders.slice(0, 2); const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1" ) or (name : "Provider 2" )'); }); test('Build KQL query with one data provider and one and', () => { @@ -113,6 +113,23 @@ describe('Build KQL Query', () => { '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' ); }); + + test('Build KQL query with all data provider', () => { + const kqlQuery = buildGlobalQuery(mockDataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" ) or (name : "Provider 2" ) or (name : "Provider 3" ) or (name : "Provider 4" ) or (name : "Provider 5" ) or (name : "Provider 6" ) or (name : "Provider 7" ) or (name : "Provider 8" ) or (name : "Provider 9" ) or (name : "Provider 10" )' + ); + }); + + test('Build complex KQL query with and and or', () => { + const dataProviders = cloneDeep(mockDataProviders); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5") or (name : "Provider 3" ) or (name : "Provider 4" ) or (name : "Provider 5" ) or (name : "Provider 6" ) or (name : "Provider 7" ) or (name : "Provider 8" ) or (name : "Provider 9" ) or (name : "Provider 10" )' + ); + }); }); describe('Combined Queries', () => { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx index 776ff114734d9..da74d22575f85 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx @@ -79,9 +79,9 @@ const buildQueryForAndProvider = ( export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => dataProviders .reduce((query, dataProvider: DataProvider, i) => { - const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; - const openParen = i > 0 ? '(' : ''; - const closeParen = i > 0 ? ')' : ''; + const prepend = (q: string) => `${q !== '' ? `${q} or ` : ''}`; + const openParen = i >= 0 && dataProviders.length > 1 ? '(' : ''; + const closeParen = i >= 0 && dataProviders.length > 1 ? ')' : ''; return dataProvider.enabled ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} ${ From e28028b36ce2051d1129d2164f2352683d1713ea Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 28 May 2020 18:36:11 -0600 Subject: [PATCH 08/38] [Maps] Fix fit to bounds requests not getting canceled (#67629) * rename data request constants * register cancel callback * clean up --- x-pack/plugins/maps/common/constants.ts | 11 +- .../public/actions/data_request_actions.ts | 107 +++++++++++++++++- x-pack/plugins/maps/public/actions/index.ts | 7 +- .../maps/public/actions/map_actions.ts | 72 ------------ .../maps/public/classes/joins/inner_join.js | 9 +- .../maps/public/classes/layers/layer.tsx | 22 +--- .../classes/layers/tile_layer/tile_layer.js | 8 +- .../tiled_vector_layer/tiled_vector_layer.tsx | 10 +- .../layers/vector_layer/vector_layer.js | 42 +++++-- .../vector_tile_layer/vector_tile_layer.js | 8 +- .../classes/sources/es_source/es_source.js | 15 ++- .../mvt_single_layer_vector_source.ts | 15 +-- .../sources/vector_source/vector_source.d.ts | 20 +++- .../properties/dynamic_style_property.js | 8 +- .../classes/styles/vector/vector_style.js | 4 +- x-pack/plugins/maps/public/reducers/map.js | 4 +- .../maps/public/selectors/map_selectors.ts | 4 +- 17 files changed, 219 insertions(+), 147 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 613e507459389..8fa44c512df4b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -72,11 +72,12 @@ export enum FIELD_ORIGIN { } export const JOIN_FIELD_NAME_PREFIX = '__kbnjoin__'; -export const SOURCE_DATA_ID_ORIGIN = 'source'; -export const META_ID_ORIGIN_SUFFIX = 'meta'; -export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; -export const FORMATTERS_ID_ORIGIN_SUFFIX = 'formatters'; -export const SOURCE_FORMATTERS_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; +export const META_DATA_REQUEST_ID_SUFFIX = 'meta'; +export const FORMATTERS_DATA_REQUEST_ID_SUFFIX = 'formatters'; +export const SOURCE_DATA_REQUEST_ID = 'source'; +export const SOURCE_META_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${META_DATA_REQUEST_ID_SUFFIX}`; +export const SOURCE_FORMATTERS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`; +export const SOURCE_BOUNDS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_bounds`; export const MIN_ZOOM = 0; export const MAX_ZOOM = 24; diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index d418214416900..13b658af6a0f3 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -6,12 +6,15 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; +// @ts-ignore +import turf from 'turf'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; -import { LAYER_TYPE, SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; +import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; import { getDataFilters, getDataRequestDescriptor, + getFittableLayers, getLayerById, getLayerList, } from '../selectors/map_selectors'; @@ -27,13 +30,15 @@ import { LAYER_DATA_LOAD_ENDED, LAYER_DATA_LOAD_ERROR, LAYER_DATA_LOAD_STARTED, + SET_GOTO, SET_LAYER_ERROR_STATUS, SET_LAYER_STYLE_META, UPDATE_LAYER_PROP, UPDATE_SOURCE_DATA_REQUEST, } from './map_action_constants'; import { ILayer } from '../classes/layers/layer'; -import { DataMeta, MapFilters } from '../../common/descriptor_types'; +import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types'; +import { DataRequestAbortError } from '../classes/util/data_request'; export type DataRequestContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -269,7 +274,7 @@ export function updateSourceDataRequest(layerId: string, newData: unknown) { return (dispatch: Dispatch) => { dispatch({ type: UPDATE_SOURCE_DATA_REQUEST, - dataId: SOURCE_DATA_ID_ORIGIN, + dataId: SOURCE_DATA_REQUEST_ID, layerId, newData, }); @@ -277,3 +282,99 @@ export function updateSourceDataRequest(layerId: string, newData: unknown) { dispatch(updateStyleMeta(layerId)); }; } + +export function fitToLayerExtent(layerId: string) { + return async (dispatch: Dispatch, getState: () => MapStoreState) => { + const targetLayer = getLayerById(layerId, getState()); + + if (targetLayer) { + try { + const bounds = await targetLayer.getBounds( + getDataRequestContext(dispatch, getState, layerId) + ); + if (bounds) { + await dispatch(setGotoWithBounds(bounds)); + } + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + // eslint-disable-next-line no-console + console.warn( + 'Unhandled getBounds error for layer. Only DataRequestAbortError should be surfaced', + error + ); + } + // new fitToLayerExtent request has superseded this thread of execution. Results no longer needed. + return; + } + } + }; +} + +export function fitToDataBounds() { + return async (dispatch: Dispatch, getState: () => MapStoreState) => { + const layerList = getFittableLayers(getState()); + + if (!layerList.length) { + return; + } + + const boundsPromises = layerList.map(async (layer: ILayer) => { + return layer.getBounds(getDataRequestContext(dispatch, getState, layer.getId())); + }); + + let bounds; + try { + bounds = await Promise.all(boundsPromises); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + // eslint-disable-next-line no-console + console.warn( + 'Unhandled getBounds error for layer. Only DataRequestAbortError should be surfaced', + error + ); + } + // new fitToDataBounds request has superseded this thread of execution. Results no longer needed. + return; + } + + const corners = []; + for (let i = 0; i < bounds.length; i++) { + const b = bounds[i]; + + // filter out undefined bounds (uses Infinity due to turf responses) + if ( + b === null || + b.minLon === Infinity || + b.maxLon === Infinity || + b.minLat === -Infinity || + b.maxLat === -Infinity + ) { + continue; + } + + corners.push([b.minLon, b.minLat]); + corners.push([b.maxLon, b.maxLat]); + } + + if (!corners.length) { + return; + } + + const turfUnionBbox = turf.bbox(turf.multiPoint(corners)); + const dataBounds = { + minLon: turfUnionBbox[0], + minLat: turfUnionBbox[1], + maxLon: turfUnionBbox[2], + maxLat: turfUnionBbox[3], + }; + + dispatch(setGotoWithBounds(dataBounds)); + }; +} + +function setGotoWithBounds(bounds: MapExtent) { + return { + type: SET_GOTO, + bounds, + }; +} diff --git a/x-pack/plugins/maps/public/actions/index.ts b/x-pack/plugins/maps/public/actions/index.ts index a2e90ff6e9f28..5b153e37da5a8 100644 --- a/x-pack/plugins/maps/public/actions/index.ts +++ b/x-pack/plugins/maps/public/actions/index.ts @@ -9,7 +9,12 @@ export * from './ui_actions'; export * from './map_actions'; export * from './map_action_constants'; export * from './layer_actions'; -export { cancelAllInFlightRequests, DataRequestContext } from './data_request_actions'; +export { + cancelAllInFlightRequests, + DataRequestContext, + fitToLayerExtent, + fitToDataBounds, +} from './data_request_actions'; export { closeOnClickTooltip, openOnClickTooltip, diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 02842edabbd2e..75df8689a670e 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -12,11 +12,9 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { - getLayerById, getDataFilters, getWaitingForMapReadyLayerListRaw, getQuery, - getFittableLayers, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -184,76 +182,6 @@ export function disableScrollZoom() { return { type: SET_SCROLL_ZOOM, scrollZoom: false }; } -export function fitToLayerExtent(layerId: string) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const targetLayer = getLayerById(layerId, getState()); - - if (targetLayer) { - const dataFilters = getDataFilters(getState()); - const bounds = await targetLayer.getBounds(dataFilters); - if (bounds) { - await dispatch(setGotoWithBounds(bounds)); - } - } - }; -} - -export function fitToDataBounds() { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const layerList = getFittableLayers(getState()); - - if (!layerList.length) { - return; - } - - const dataFilters = getDataFilters(getState()); - const boundsPromises = layerList.map(async (layer) => { - return layer.getBounds(dataFilters); - }); - - const bounds = await Promise.all(boundsPromises); - const corners = []; - for (let i = 0; i < bounds.length; i++) { - const b = bounds[i]; - - // filter out undefined bounds (uses Infinity due to turf responses) - if ( - b === null || - b.minLon === Infinity || - b.maxLon === Infinity || - b.minLat === -Infinity || - b.maxLat === -Infinity - ) { - continue; - } - - corners.push([b.minLon, b.minLat]); - corners.push([b.maxLon, b.maxLat]); - } - - if (!corners.length) { - return; - } - - const turfUnionBbox = turf.bbox(turf.multiPoint(corners)); - const dataBounds = { - minLon: turfUnionBbox[0], - minLat: turfUnionBbox[1], - maxLon: turfUnionBbox[2], - maxLat: turfUnionBbox[3], - }; - - dispatch(setGotoWithBounds(dataBounds)); - }; -} - -export function setGotoWithBounds(bounds: MapExtent) { - return { - type: SET_GOTO, - bounds, - }; -} - export function setGotoWithCenter({ lat, lon, zoom }: MapCenterAndZoom) { return { type: SET_GOTO, diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.js b/x-pack/plugins/maps/public/classes/joins/inner_join.js index 5f8bc7385d04c..76afe2430b818 100644 --- a/x-pack/plugins/maps/public/classes/joins/inner_join.js +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.js @@ -6,7 +6,10 @@ import { ESTermSource } from '../sources/es_term_source'; import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; -import { META_ID_ORIGIN_SUFFIX, FORMATTERS_ID_ORIGIN_SUFFIX } from '../../../common/constants'; +import { + META_DATA_REQUEST_ID_SUFFIX, + FORMATTERS_DATA_REQUEST_ID_SUFFIX, +} from '../../../common/constants'; export class InnerJoin { constructor(joinDescriptor, leftSource) { @@ -42,11 +45,11 @@ export class InnerJoin { } getSourceMetaDataRequestId() { - return `${this.getSourceDataRequestId()}_${META_ID_ORIGIN_SUFFIX}`; + return `${this.getSourceDataRequestId()}_${META_DATA_REQUEST_ID_SUFFIX}`; } getSourceFormattersDataRequestId() { - return `${this.getSourceDataRequestId()}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; + return `${this.getSourceDataRequestId()}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`; } getLeftField() { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 5d54166e08fb7..2250d5663378c 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -17,21 +17,16 @@ import { MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, - SOURCE_DATA_ID_ORIGIN, + SOURCE_DATA_REQUEST_ID, } from '../../../common/constants'; import { copyPersistentState } from '../../reducers/util'; -import { - LayerDescriptor, - MapExtent, - MapFilters, - StyleDescriptor, -} from '../../../common/descriptor_types'; +import { LayerDescriptor, MapExtent, StyleDescriptor } from '../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; export interface ILayer { - getBounds(mapFilters: MapFilters): Promise; + getBounds(dataRequestContext: DataRequestContext): Promise; getDataRequest(id: string): DataRequest | undefined; getDisplayName(source?: ISource): Promise; getId(): string; @@ -401,7 +396,7 @@ export class AbstractLayer implements ILayer { } getSourceDataRequest(): DataRequest | undefined { - return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); + return this.getDataRequest(SOURCE_DATA_REQUEST_ID); } getDataRequest(id: string): DataRequest | undefined { @@ -455,13 +450,8 @@ export class AbstractLayer implements ILayer { return sourceDataRequest ? sourceDataRequest.hasData() : false; } - async getBounds(mapFilters: MapFilters): Promise { - return { - minLon: -180, - maxLon: 180, - minLat: -89, - maxLat: 89, - }; + async getBounds(dataRequestContext: DataRequestContext): Promise { + return null; } renderStyleEditor({ diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js index 5108e5cd3e6a3..cd8ca44d4478b 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js @@ -6,7 +6,7 @@ import { AbstractLayer } from '../layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; import { TileStyle } from '../../styles/tile/tile_style'; export class TileLayer extends AbstractLayer { @@ -31,12 +31,12 @@ export class TileLayer extends AbstractLayer { return; } const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); - startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, dataFilters); try { const url = await this.getSource().getUrlTemplate(); - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, url, {}); + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, url, {}); } catch (error) { - onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); } } diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 812b7b409fe12..a00639aa5fec5 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../../../common/constants'; +import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants'; import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { ITiledSingleLayerVectorSource } from '../../sources/vector_source'; @@ -56,7 +56,7 @@ export class TiledVectorLayer extends VectorLayer { onLoadError, dataFilters, }: DataRequestContext) { - const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_ID_ORIGIN}`); + const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`); const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( dataFilters, this.getSource(), @@ -73,12 +73,12 @@ export class TiledVectorLayer extends VectorLayer { return null; } - startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters); try { const templateWithMeta = await this._source.getUrlTemplateWithMeta(); - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, templateWithMeta, {}); + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, templateWithMeta, {}); } catch (error) { - onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index a2a0e58e48fd2..524ab245c6760 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -9,9 +9,10 @@ import { AbstractLayer } from '../layer'; import { VectorStyle } from '../../styles/vector/vector_style'; import { FEATURE_ID_PROPERTY_NAME, - SOURCE_DATA_ID_ORIGIN, - SOURCE_META_ID_ORIGIN, - SOURCE_FORMATTERS_ID_ORIGIN, + SOURCE_DATA_REQUEST_ID, + SOURCE_META_DATA_REQUEST_ID, + SOURCE_FORMATTERS_DATA_REQUEST_ID, + SOURCE_BOUNDS_DATA_REQUEST_ID, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, LAYER_TYPE, @@ -155,18 +156,41 @@ export class VectorLayer extends AbstractLayer { return this.getCurrentStyle().renderLegendDetails(); } - async getBounds(dataFilters) { + async getBounds({ startLoading, stopLoading, registerCancelCallback, dataFilters }) { const isStaticLayer = !this.getSource().isBoundsAware(); if (isStaticLayer) { return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this._hasJoins()); } + const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${this.getId()}`); const searchFilters = this._getSearchFilters( dataFilters, this.getSource(), this.getCurrentStyle() ); - return await this.getSource().getBoundsForFilters(searchFilters); + // Do not pass all searchFilters to source.getBoundsForFilters(). + // For example, do not want to filter bounds request by extent and buffer. + const boundsFilters = { + sourceQuery: searchFilters.sourceQuery, + query: searchFilters.query, + timeFilters: searchFilters.timeFilters, + filters: searchFilters.filters, + applyGlobalQuery: searchFilters.applyGlobalQuery, + }; + + let bounds = null; + try { + startLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, boundsFilters); + bounds = await this.getSource().getBoundsForFilters( + boundsFilters, + registerCancelCallback.bind(null, requestToken) + ); + } finally { + // Use stopLoading callback instead of onLoadError callback. + // Function is loading bounds and not feature data. + stopLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, bounds, boundsFilters); + } + return bounds; } async getLeftJoinFields() { @@ -342,7 +366,7 @@ export class VectorLayer extends AbstractLayer { dataFilters, isRequestStillActive, } = syncContext; - const dataRequestId = SOURCE_DATA_ID_ORIGIN; + const dataRequestId = SOURCE_DATA_REQUEST_ID; const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); const searchFilters = this._getSearchFilters(dataFilters, source, style); const prevDataRequest = this.getSourceDataRequest(); @@ -394,7 +418,7 @@ export class VectorLayer extends AbstractLayer { source, style, sourceQuery: this.getQuery(), - dataRequestId: SOURCE_META_ID_ORIGIN, + dataRequestId: SOURCE_META_DATA_REQUEST_ID, dynamicStyleProps: style.getDynamicPropertiesArray().filter((dynamicStyleProp) => { return ( dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && @@ -470,7 +494,7 @@ export class VectorLayer extends AbstractLayer { layerName, style, dynamicStyleProps, - registerCancelCallback, + registerCancelCallback.bind(null, requestToken), nextMeta ); stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); @@ -488,7 +512,7 @@ export class VectorLayer extends AbstractLayer { return this._syncFormatters({ source, - dataRequestId: SOURCE_FORMATTERS_ID_ORIGIN, + dataRequestId: SOURCE_FORMATTERS_DATA_REQUEST_ID, fields: style .getDynamicPropertiesArray() .filter((dynamicStyleProp) => { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js index 6f616afb64041..61ec02e72adf2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js @@ -6,7 +6,7 @@ import { TileLayer } from '../tile_layer/tile_layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; import { isRetina } from '../../../meta'; import { addSpriteSheetToMapFromImageData, @@ -56,16 +56,16 @@ export class VectorTileLayer extends TileLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); try { - startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, dataFilters); const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); const data = { ...styleAndSprites, spriteSheetImageData, }; - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, data, nextMeta); + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data, nextMeta); } catch (error) { - onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index 072f952fb8a13..450894d81485c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -145,11 +145,8 @@ export class AbstractESSource extends AbstractVectorSource { return searchSource; } - async getBoundsForFilters({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }) { - const searchSource = await this.makeSearchSource( - { sourceQuery, query, timeFilters, filters, applyGlobalQuery }, - 0 - ); + async getBoundsForFilters(boundsFilters, registerCancelCallback) { + const searchSource = await this.makeSearchSource(boundsFilters, 0); searchSource.setField('aggs', { fitToBounds: { geo_bounds: { @@ -160,13 +157,19 @@ export class AbstractESSource extends AbstractVectorSource { let esBounds; try { - const esResp = await searchSource.fetch(); + const abortController = new AbortController(); + registerCancelCallback(() => abortController.abort()); + const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); if (!esResp.aggregations.fitToBounds.bounds) { // aggregations.fitToBounds is empty object when there are no matching documents return null; } esBounds = esResp.aggregations.fitToBounds.bounds; } catch (error) { + if (error.name === 'AbortError') { + throw new DataRequestAbortError(); + } + return null; } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 5f6061b38678c..86a1589a7a030 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { AbstractSource, ImmutableSourceProperty } from '../source'; -import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { IField } from '../../fields/field'; @@ -16,7 +16,6 @@ import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters import { MapExtent, TiledSingleLayerVectorSourceDescriptor, - VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; @@ -133,13 +132,11 @@ export class MVTSingleLayerVectorSource extends AbstractSource return this._descriptor.maxSourceZoom; } - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent { - return { - maxLat: 90, - maxLon: 180, - minLat: -90, - minLon: -180, - }; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null { + return null; } getFieldByName(fieldName: string): IField | null { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 2dd6bcd858137..711b7d600d74d 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -6,11 +6,13 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { FeatureCollection } from 'geojson'; +import { Filter, TimeRange } from 'src/plugins/data/public'; import { AbstractSource, ISource } from '../source'; import { IField } from '../../fields/field'; import { ESSearchSourceResponseMeta, MapExtent, + MapQuery, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; @@ -24,9 +26,20 @@ export type GeoJsonWithMeta = { meta?: GeoJsonFetchMeta; }; +export type BoundsFilters = { + applyGlobalQuery: boolean; + filters: Filter[]; + query: MapQuery; + sourceQuery: MapQuery; + timeFilters: TimeRange; +}; + export interface IVectorSource extends ISource { filterAndFormatPropertiesToHtml(properties: unknown): Promise; - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null; getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], @@ -42,7 +55,10 @@ export interface IVectorSource extends ISource { export class AbstractVectorSource extends AbstractSource implements IVectorSource { filterAndFormatPropertiesToHtml(properties: unknown): Promise; - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null; getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js index 98d5d3feb60ea..15d0b3c4bf913 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js @@ -7,7 +7,11 @@ import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; -import { STYLE_TYPE, SOURCE_META_ID_ORIGIN, FIELD_ORIGIN } from '../../../../../common/constants'; +import { + STYLE_TYPE, + SOURCE_META_DATA_REQUEST_ID, + FIELD_ORIGIN, +} from '../../../../../common/constants'; import React from 'react'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover'; @@ -30,7 +34,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { _getStyleMetaDataRequestId(fieldName) { if (this.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { - return SOURCE_META_ID_ORIGIN; + return SOURCE_META_DATA_REQUEST_ID; } const join = this._layer.getValidJoins().find((join) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js index f3ed18bd1302e..989ac268c0552 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js @@ -13,7 +13,7 @@ import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE, - SOURCE_FORMATTERS_ID_ORIGIN, + SOURCE_FORMATTERS_DATA_REQUEST_ID, LAYER_STYLE_TYPE, DEFAULT_ICON, VECTOR_STYLES, @@ -373,7 +373,7 @@ export class VectorStyle extends AbstractStyle { let dataRequestId; if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { - dataRequestId = SOURCE_FORMATTERS_ID_ORIGIN; + dataRequestId = SOURCE_FORMATTERS_DATA_REQUEST_ID; } else { const join = this._layer.getValidJoins().find((join) => { return join.getRightJoinSource().hasMatchingMetricField(fieldName); diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 9a661fe4833a8..317c11eb7680c 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -53,7 +53,7 @@ import { import { getDefaultMapSettings } from './default_map_settings'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util'; -import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; +import { SOURCE_DATA_REQUEST_ID } from '../../common/constants'; const getLayerIndex = (list, layerId) => list.findIndex(({ id }) => layerId === id); @@ -443,7 +443,7 @@ function updateSourceDataRequest(state, action) { return state; } const dataRequest = layerDescriptor.__dataRequests.find((dataRequest) => { - return dataRequest.dataId === SOURCE_DATA_ID_ORIGIN; + return dataRequest.dataId === SOURCE_DATA_REQUEST_ID; }); if (!dataRequest) { return state; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index fd887d360c2e0..467f1074e88e7 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -26,7 +26,7 @@ import { getSourceByType } from '../classes/sources/source_registry'; import { GeojsonFileSource } from '../classes/sources/client_file_source'; import { LAYER_TYPE, - SOURCE_DATA_ID_ORIGIN, + SOURCE_DATA_REQUEST_ID, STYLE_TYPE, VECTOR_STYLES, SPATIAL_FILTERS_LAYER_ID, @@ -263,7 +263,7 @@ export const getSpatialFiltersLayer = createSelector( alpha: settings.spatialFiltersAlpa, __dataRequests: [ { - dataId: SOURCE_DATA_ID_ORIGIN, + dataId: SOURCE_DATA_REQUEST_ID, data: featureCollection, }, ], From b9d1cec7fd1d72e69bbacd1ef6261f4eb57bc812 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 29 May 2020 06:33:17 +0200 Subject: [PATCH 09/38] [Discover] Improve a11y test when switching to context (#67363) --- test/accessibility/apps/discover.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 38552f5ecdafe..7e905fbe89fbd 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - // FLAKY: https://github.com/elastic/kibana/issues/62497 - describe.skip('Discover', () => { + describe('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -133,9 +132,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Context view test it('should open context view on a doc', async () => { - await docTable.clickRowToggle(); - // click the open action await retry.try(async () => { + await docTable.clickRowToggle(); + // click the open action const rowActions = await docTable.getRowActions(); if (!rowActions.length) { throw new Error('row actions empty, trying again'); From cc83cfa3c7b9e33d3b3a30d759edca123e99cd31 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 29 May 2020 10:14:29 +0300 Subject: [PATCH 10/38] Fix bug on vis metric regarding applying the light theme when thebg is dark (#67481) --- .../vis_type_metric/public/components/metric_vis_component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx index eb3986b6388fe..5e8a463748188 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -93,7 +93,7 @@ export class MetricVisComponent extends Component { return false; } - const [red, green, blue] = colors.slice(1).map(parseInt); + const [red, green, blue] = colors.slice(1).map((c) => parseInt(c, 10)); return isColorDark(red, green, blue); } From 9c2866144902b5172d411bb2ba65680adaca1970 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 29 May 2020 09:34:55 +0200 Subject: [PATCH 11/38] perf: drag and drop performance improvement for field list (#67455) --- .../lens/public/drag_drop/drag_drop.test.tsx | 6 ++-- .../lens/public/drag_drop/drag_drop.tsx | 36 ++++++++++++++++--- .../indexpattern_datasource/field_item.tsx | 10 ++++-- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index 6bf629912f53c..765522067eaf0 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render, shallow, mount } from 'enzyme'; +import { render, mount } from 'enzyme'; import { DragDrop } from './drag_drop'; import { ChildDragDropProvider } from './providers'; @@ -24,7 +24,7 @@ describe('DragDrop', () => { test('dragover calls preventDefault if droppable is true', () => { const preventDefault = jest.fn(); - const component = shallow(Hello!); + const component = mount(Hello!); component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); @@ -33,7 +33,7 @@ describe('DragDrop', () => { test('dragover does not call preventDefault if droppable is false', () => { const preventDefault = jest.fn(); - const component = shallow(Hello!); + const component = mount(Hello!); component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 72b0d58122405..5a0fc3b3839f7 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -88,11 +88,39 @@ type Props = DraggableProps | NonDraggableProps; * * @param props */ -export function DragDrop(props: Props) { + +export const DragDrop = (props: Props) => { const { dragging, setDragging } = useContext(DragContext); + const { value, draggable, droppable } = props; + return ( + + ); +}; + +const DragDropInner = React.memo(function DragDropInner( + props: Props & { + dragging: unknown; + setDragging: (dragging: unknown) => void; + isDragging: boolean; + } +) { const [state, setState] = useState({ isActive: false }); - const { className, onDrop, value, children, droppable, draggable } = props; - const isDragging = draggable && value === dragging; + const { + className, + onDrop, + value, + children, + droppable, + draggable, + dragging, + setDragging, + isDragging, + } = props; const classes = classNames('lnsDragDrop', className, { 'lnsDragDrop-isDropTarget': droppable, @@ -166,4 +194,4 @@ export function DragDrop(props: Props) { {children} ); -} +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 81eb53cd10002..6c00706cc8609 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -78,7 +78,7 @@ function wrapOnDot(str?: string) { return str ? str.replace(/\./g, '.\u200B') : ''; } -export function FieldItem(props: FieldItemProps) { +export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) { const { core, field, @@ -170,6 +170,10 @@ export function FieldItem(props: FieldItemProps) { } } + const value = React.useMemo(() => ({ field, indexPatternId: indexPattern.id } as DraggedField), [ + field, + indexPattern.id, + ]); return ( ); -} +}); function FieldItemPopoverContents(props: State & FieldItemProps) { const { From 84ed5096f3631d36a1f15f81202ffaa18e376725 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 29 May 2020 09:38:07 +0200 Subject: [PATCH 12/38] [Lens] Fix empty values filtering (#67594) --- .../datatable_visualization/expression.tsx | 4 +- .../pie_visualization/render_function.tsx | 3 +- x-pack/plugins/lens/public/utils.test.ts | 111 ++++++++++++++++++ x-pack/plugins/lens/public/utils.ts | 39 ++++++ .../public/xy_visualization/xy_expression.tsx | 3 +- 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/lens/public/utils.test.ts create mode 100644 x-pack/plugins/lens/public/utils.ts diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 1fd01654d8149..143bec227ebee 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -22,7 +22,7 @@ import { } from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; import { EmptyPlaceholder } from '../shared_components'; - +import { desanitizeFilterContext } from '../utils'; export interface DatatableColumns { columnIds: string[]; } @@ -180,7 +180,7 @@ export function DatatableComponent(props: DatatableRenderProps) { ], timeFieldName, }; - props.onClickValue(data); + props.onClickValue(desanitizeFilterContext(data)); }, [firstTable] ); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index be74ec352287f..36e8d9660ab70 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -31,6 +31,7 @@ import { ColumnGroups, PieExpressionProps } from './types'; import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; +import { desanitizeFilterContext } from '../utils'; const EMPTY_SLICE = Symbol('empty_slice'); @@ -242,7 +243,7 @@ export function PieComponent( firstTable ); - onClickValue(context); + onClickValue(desanitizeFilterContext(context)); }} /> { + it(`When filtered value equals '(empty)' replaces it with '' in table and in value.`, () => { + const table = { + rows: [ + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + ], + columns: [ + { + id: 'f903668f-1175-4705-a5bd-713259d10326', + name: 'order_date per 30 seconds', + }, + { + id: '5d5446b2-72e8-4f86-91e0-88380f0fa14c', + name: 'Top values of customer_phone', + }, + { + id: '9f0b6f88-c399-43a0-a993-0ad943c9af25', + name: 'Count of records', + }, + ], + }; + + const contextWithEmptyValue: LensFilterEvent['data'] = { + data: [ + { + row: 3, + column: 0, + value: 1589414910000, + table, + }, + { + row: 0, + column: 1, + value: '(empty)', + table, + }, + ], + timeFieldName: 'order_date', + }; + + const desanitizedFilterContext = desanitizeFilterContext(contextWithEmptyValue); + + expect(desanitizedFilterContext).toEqual({ + data: [ + { + row: 3, + column: 0, + value: 1589414910000, + table, + }, + { + value: '', + row: 0, + column: 1, + table: { + rows: [ + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + ], + columns: table.columns, + }, + }, + ], + timeFieldName: 'order_date', + }); + }); +}); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts new file mode 100644 index 0000000000000..171707dcb9d26 --- /dev/null +++ b/x-pack/plugins/lens/public/utils.ts @@ -0,0 +1,39 @@ +/* + * 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 { LensFilterEvent } from './types'; + +/** replaces the value `(empty) to empty string for proper filtering` */ +export const desanitizeFilterContext = ( + context: LensFilterEvent['data'] +): LensFilterEvent['data'] => { + const emptyTextValue = i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { + defaultMessage: '(empty)', + }); + return { + ...context, + data: context.data.map((point) => + point.value === emptyTextValue + ? { + ...point, + value: '', + table: { + ...point.table, + rows: point.table.rows.map((row, index) => + index === point.row + ? { + ...row, + [point.table.columns[point.column].id]: '', + } + : row + ), + }, + } + : point + ), + }; +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 4ad2b2f22c98a..003036b211f03 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -39,6 +39,7 @@ import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; import { parseInterval } from '../../../../../src/plugins/data/common'; import { EmptyPlaceholder } from '../shared_components'; +import { desanitizeFilterContext } from '../utils'; type InferPropType = T extends React.FunctionComponent ? P : T; type SeriesSpec = InferPropType & @@ -354,7 +355,7 @@ export function XYChart({ })), timeFieldName, }; - onClickValue(context); + onClickValue(desanitizeFilterContext(context)); }} /> From d9ac0489a3b28d3212a488e7a28d0061a587c586 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 29 May 2020 14:07:58 +0200 Subject: [PATCH 13/38] [APM] Correctly format url when linking to other apps (#67446) Co-authored-by: Elastic Machine --- .../TransactionActionMenu.tsx | 7 +- .../__test__/TransactionActionMenu.test.tsx | 138 ++++++++++++++++-- .../shared/TransactionActionMenu/sections.ts | 2 +- 3 files changed, 130 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index dc4413ee98360..988edb197a230 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -105,10 +105,11 @@ export const TransactionActionMenu: FunctionComponent = ({ if (app === 'uptime' || app === 'metrics' || app === 'logs') { event.preventDefault(); + const search = parsed.search || ''; + + const path = `${rest.join('/')}${search}`; core.application.navigateToApp(app, { - path: `${rest.join('/')}${ - parsed.search ? `&${parsed.search}` : '' - }`, + path, }); } }, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 718f81b3c1027..bad9292f3e768 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; +import { merge, tail } from 'lodash'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import * as Transactions from './mockData'; @@ -16,13 +17,43 @@ import { import * as hooks from '../../../../hooks/useFetcher'; import { LicenseContext } from '../../../../context/LicenseContext'; import { License } from '../../../../../../licensing/common/license'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { + MockApmPluginContextWrapper, + mockApmPluginContextValue, +} from '../../../../context/ApmPluginContext/MockApmPluginContext'; import * as apmApi from '../../../../services/rest/createCallApmApi'; +import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; + +const getMock = () => { + return (merge({}, mockApmPluginContextValue, { + core: { + application: { + navigateToApp: jest.fn(), + }, + http: { + basePath: { + remove: jest.fn((path: string) => { + return tail(path.split('/')).join('/'); + }), + }, + }, + }, + }) as unknown) as ApmPluginContextValue; +}; -const renderTransaction = async (transaction: Record) => { +const renderTransaction = async ( + transaction: Record, + mock: ApmPluginContextValue = getMock() +) => { const rendered = render( , - { wrapper: MockApmPluginContextWrapper } + { + wrapper: ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ), + } ); fireEvent.click(rendered.getByText('Actions')); @@ -49,11 +80,21 @@ describe('TransactionActionMenu component', () => { }); it('should always render the trace logs link', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithMinimalData + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithMinimalData, + mock ); expect(queryByText('Trace logs')).not.toBeNull(); + + fireEvent.click(getByText('Trace logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: + 'link-to/logs?time=1545092070952&filter=trace.id:%228b60bd32ecc6e1506735a8b6cfcf175c%22%20OR%208b60bd32ecc6e1506735a8b6cfcf175c', + }); }); it('should not render the pod links when there is no pod id', async () => { @@ -66,12 +107,33 @@ describe('TransactionActionMenu component', () => { }); it('should render the pod links when there is a pod id', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithKubernetesData + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithKubernetesData, + mock ); expect(queryByText('Pod logs')).not.toBeNull(); expect(queryByText('Pod metrics')).not.toBeNull(); + + fireEvent.click(getByText('Pod logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: 'link-to/pod-logs/pod123456abcdef?time=1545092070952', + }); + + (mock.core.application.navigateToApp as jest.Mock).mockClear(); + + fireEvent.click(getByText('Pod metrics')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith( + 'metrics', + { + path: + 'link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952', + } + ); }); it('should not render the container links when there is no container id', async () => { @@ -84,12 +146,33 @@ describe('TransactionActionMenu component', () => { }); it('should render the container links when there is a container id', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithContainerData + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithContainerData, + mock ); expect(queryByText('Container logs')).not.toBeNull(); expect(queryByText('Container metrics')).not.toBeNull(); + + fireEvent.click(getByText('Container logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: 'link-to/container-logs/container123456abcdef?time=1545092070952', + }); + + (mock.core.application.navigateToApp as jest.Mock).mockClear(); + + fireEvent.click(getByText('Container metrics')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith( + 'metrics', + { + path: + 'link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952', + } + ); }); it('should not render the host links when there is no hostname', async () => { @@ -102,12 +185,32 @@ describe('TransactionActionMenu component', () => { }); it('should render the host links when there is a hostname', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithHostData + const mock = getMock(); + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithHostData, + mock ); expect(queryByText('Host logs')).not.toBeNull(); expect(queryByText('Host metrics')).not.toBeNull(); + + fireEvent.click(getByText('Host logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: 'link-to/host-logs/227453131a17?time=1545092070952', + }); + + (mock.core.application.navigateToApp as jest.Mock).mockClear(); + + fireEvent.click(getByText('Host metrics')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith( + 'metrics', + { + path: + 'link-to/host-detail/227453131a17?from=1545091770952&to=1545092370952', + } + ); }); it('should not render the uptime link if there is no url available', async () => { @@ -127,11 +230,20 @@ describe('TransactionActionMenu component', () => { }); it('should render the uptime link if there is a url with a domain', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithUrlAndDomain + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithUrlAndDomain, + mock ); expect(queryByText('Status')).not.toBeNull(); + + fireEvent.click(getByText('Status')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('uptime', { + path: '?search=url.domain:%22example.com%22', + }); }); it('should match the snapshot', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index 60cedfde24258..7f99939a0a0d0 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -62,7 +62,7 @@ export const getSections = ({ const uptimeLink = url.format({ pathname: basePath.prepend('/app/uptime'), - hash: `/?${fromQuery( + search: `?${fromQuery( pick( { dateRangeStart: urlParams.rangeFrom, From 0712741bb361ecd1683ca5f8e7a7e010e1864617 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 29 May 2020 17:07:45 +0300 Subject: [PATCH 14/38] [SIEM][CASE] Fix callout messages appearance (#67303) Co-authored-by: Elastic Machine --- .../cases/components/callout/index.test.tsx | 37 ++++++++++++++++ .../public/cases/components/callout/index.tsx | 2 +- .../use_push_to_service/index.test.tsx | 43 ++++++++++++++++++- .../components/use_push_to_service/index.tsx | 2 +- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx index c830a6f5e10d5..ee3faeb2ceeb5 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx @@ -60,6 +60,43 @@ describe('CaseCallOut ', () => { ).toBeTruthy(); }); + it('it applies the correct color to button', () => { + const props = { + ...defaultProps, + messages: [ + { + ...defaultProps, + description:

{'one'}

, + errorType: 'danger' as 'primary' | 'success' | 'warning' | 'danger', + }, + { + ...defaultProps, + description:

{'two'}

, + errorType: 'success' as 'primary' | 'success' | 'warning' | 'danger', + }, + { + ...defaultProps, + description:

{'three'}

, + errorType: 'primary' as 'primary' | 'success' | 'warning' | 'danger', + }, + ], + }; + + const wrapper = mount(); + + expect(wrapper.find(`[data-test-subj="callout-dismiss-danger"]`).first().prop('color')).toBe( + 'danger' + ); + + expect(wrapper.find(`[data-test-subj="callout-dismiss-success"]`).first().prop('color')).toBe( + 'secondary' + ); + + expect(wrapper.find(`[data-test-subj="callout-dismiss-primary"]`).first().prop('color')).toBe( + 'primary' + ); + }); + it('Dismisses callout', () => { const props = { ...defaultProps, diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.tsx index 470b03637dc00..171c0508b9d92 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.tsx @@ -66,7 +66,7 @@ const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => )} {i18n.DISMISS_CALLOUT} diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx index e3e627e3a136e..4391db1a0a0a1 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx @@ -123,7 +123,27 @@ describe('usePushToService', () => { }); }); - it('Displays message when user does not have a connector configured', async () => { + it('Displays message when user does not have any connector configured', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connectors: [], + caseConnectorId: 'none', + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE); + }); + }); + + it('Displays message when user does have a connector but is configured to none', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -162,6 +182,27 @@ describe('usePushToService', () => { }); }); + it('Displays message when connector is deleted with empty connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connectors: [], + caseConnectorId: 'not-exist', + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx index 5d238b623eb4a..63b808eed3c92 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx @@ -75,7 +75,7 @@ export const usePushToService = ({ if (actionLicense != null && !actionLicense.enabledInLicense) { errors = [...errors, getLicenseError()]; } - if (connectors.length === 0 && !loadingLicense) { + if (connectors.length === 0 && caseConnectorId === 'none' && !loadingLicense) { errors = [ ...errors, { From ae724f10356d92060e6a1a4969aed106736038f0 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Fri, 29 May 2020 10:12:51 -0400 Subject: [PATCH 15/38] [SIEMDPOINT][WIP] Add Management section and move Policy related views (#67417) * Add Management top-level nav tab item * Move of Policy related views to `management` * Enhance PageView component to support sub-tabs --- .../siem/public/app/home/home_navigations.tsx | 8 + .../siem/public/app/home/translations.ts | 4 + x-pack/plugins/siem/public/app/types.ts | 18 +-- .../__snapshots__/page_view.test.tsx.snap | 32 ++++ .../common/components/endpoint/page_view.tsx | 149 +++++++++++------- .../components/header_global/index.test.tsx | 1 + .../common/components/link_to/link_to.tsx | 5 + .../link_to/redirect_to_management.tsx | 15 ++ .../components/navigation/index.test.tsx | 15 ++ .../common/components/url_state/constants.ts | 10 +- .../common/components/url_state/types.ts | 5 +- .../mock/endpoint/app_context_render.tsx | 14 +- .../siem/public/common/mock/global_state.ts | 9 +- .../plugins/siem/public/common/mock/utils.ts | 6 +- .../siem/public/common/store/actions.ts | 4 +- .../siem/public/common/store/reducer.ts | 20 +-- .../plugins/siem/public/common/store/types.ts | 11 ++ .../siem/public/endpoint_alerts/index.ts | 10 +- .../siem/public/endpoint_hosts/index.ts | 10 +- .../siem/public/endpoint_policy/details.ts | 41 ----- .../siem/public/endpoint_policy/list.ts | 41 ----- .../siem/public/endpoint_policy/routes.tsx | 18 --- .../endpoint_policy/view/policy_hooks.ts | 19 --- .../public/management/common/constants.ts | 21 +++ .../siem/public/management/common/routing.ts | 70 ++++++++ .../components/management_page_view.tsx | 37 +++++ .../plugins/siem/public/management/index.ts | 39 +++++ .../siem/public/management/pages/index.tsx | 43 +++++ .../public/management/pages/policy/index.tsx | 26 +++ .../policy}/models/policy_details_config.ts | 2 +- .../policy}/store/policy_details/action.ts | 6 +- .../store/policy_details/index.test.ts | 2 +- .../policy}/store/policy_details/index.ts | 6 +- .../store/policy_details/middleware.ts | 6 +- .../policy}/store/policy_details/reducer.ts | 6 +- .../policy}/store/policy_details/selectors.ts | 29 ++-- .../pages/policy}/store/policy_list/action.ts | 4 +- .../policy}/store/policy_list/index.test.ts | 23 +-- .../pages/policy}/store/policy_list/index.ts | 6 +- .../policy}/store/policy_list/middleware.ts | 4 +- .../policy}/store/policy_list/reducer.ts | 6 +- .../policy}/store/policy_list/selectors.ts | 11 +- .../store/policy_list/services/ingest.test.ts | 4 +- .../store/policy_list/services/ingest.ts | 4 +- .../store/policy_list/test_mock_utils.ts | 2 +- .../pages/policy}/types.ts | 6 +- .../pages/policy}/view/agents_summary.tsx | 0 .../pages/policy}/view/index.ts | 0 .../policy}/view/policy_details.test.tsx | 26 +-- .../pages/policy}/view/policy_details.tsx | 29 ++-- .../policy}/view/policy_forms/config_form.tsx | 0 .../view/policy_forms/events/checkbox.tsx | 2 +- .../view/policy_forms/events/index.tsx | 0 .../view/policy_forms/events/linux.tsx | 2 +- .../policy}/view/policy_forms/events/mac.tsx | 2 +- .../view/policy_forms/events/windows.tsx | 2 +- .../view/policy_forms/protections/malware.tsx | 2 +- .../pages/policy/view/policy_hooks.ts | 44 ++++++ .../pages/policy}/view/policy_list.tsx | 32 ++-- .../pages/policy}/view/vertical_divider.ts | 2 +- .../plugins/siem/public/management/routes.tsx | 18 +++ .../siem/public/management/store/index.ts | 8 + .../public/management/store/middleware.ts | 33 ++++ .../siem/public/management/store/reducer.ts | 45 ++++++ .../siem/public/management/store/types.ts | 26 +++ .../plugins/siem/public/management/types.ts | 36 +++++ x-pack/plugins/siem/public/plugin.tsx | 41 ++--- 67 files changed, 815 insertions(+), 363 deletions(-) create mode 100644 x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx delete mode 100644 x-pack/plugins/siem/public/endpoint_policy/details.ts delete mode 100644 x-pack/plugins/siem/public/endpoint_policy/list.ts delete mode 100644 x-pack/plugins/siem/public/endpoint_policy/routes.tsx delete mode 100644 x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts create mode 100644 x-pack/plugins/siem/public/management/common/constants.ts create mode 100644 x-pack/plugins/siem/public/management/common/routing.ts create mode 100644 x-pack/plugins/siem/public/management/components/management_page_view.tsx create mode 100644 x-pack/plugins/siem/public/management/index.ts create mode 100644 x-pack/plugins/siem/public/management/pages/index.tsx create mode 100644 x-pack/plugins/siem/public/management/pages/policy/index.tsx rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/models/policy_details_config.ts (96%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_details/action.ts (87%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_details/index.test.ts (96%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_details/index.ts (77%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_details/middleware.ts (93%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_details/reducer.ts (95%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_details/selectors.ts (87%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/action.ts (83%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/index.test.ts (90%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/index.ts (76%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/middleware.ts (91%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/reducer.ts (89%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/selectors.ts (87%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/services/ingest.test.ts (94%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/services/ingest.ts (95%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/store/policy_list/test_mock_utils.ts (93%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/types.ts (96%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/agents_summary.tsx (100%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/index.ts (100%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_details.test.tsx (90%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_details.tsx (90%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/config_form.tsx (100%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/events/checkbox.tsx (95%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/events/index.tsx (100%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/events/linux.tsx (97%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/events/mac.tsx (97%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/events/windows.tsx (98%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_forms/protections/malware.tsx (98%) create mode 100644 x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/policy_list.tsx (84%) rename x-pack/plugins/siem/public/{endpoint_policy => management/pages/policy}/view/vertical_divider.ts (92%) create mode 100644 x-pack/plugins/siem/public/management/routes.tsx create mode 100644 x-pack/plugins/siem/public/management/store/index.ts create mode 100644 x-pack/plugins/siem/public/management/store/middleware.ts create mode 100644 x-pack/plugins/siem/public/management/store/reducer.ts create mode 100644 x-pack/plugins/siem/public/management/store/types.ts create mode 100644 x-pack/plugins/siem/public/management/types.ts diff --git a/x-pack/plugins/siem/public/app/home/home_navigations.tsx b/x-pack/plugins/siem/public/app/home/home_navigations.tsx index 2eed64a2b26e5..bb9e99326182f 100644 --- a/x-pack/plugins/siem/public/app/home/home_navigations.tsx +++ b/x-pack/plugins/siem/public/app/home/home_navigations.tsx @@ -14,6 +14,7 @@ import { } from '../../common/components/link_to'; import * as i18n from './translations'; import { SiemPageName, SiemNavTab } from '../types'; +import { getManagementUrl } from '../../management'; export const navTabs: SiemNavTab = { [SiemPageName.overview]: { @@ -58,4 +59,11 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'case', }, + [SiemPageName.management]: { + id: SiemPageName.management, + name: i18n.MANAGEMENT, + href: getManagementUrl({ name: 'default' }), + disabled: false, + urlKey: SiemPageName.management, + }, }; diff --git a/x-pack/plugins/siem/public/app/home/translations.ts b/x-pack/plugins/siem/public/app/home/translations.ts index f2bcaa07b1a25..0cce45b4cef27 100644 --- a/x-pack/plugins/siem/public/app/home/translations.ts +++ b/x-pack/plugins/siem/public/app/home/translations.ts @@ -29,3 +29,7 @@ export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { export const CASE = i18n.translate('xpack.siem.navigation.case', { defaultMessage: 'Cases', }); + +export const MANAGEMENT = i18n.translate('xpack.siem.navigation.management', { + defaultMessage: 'Management', +}); diff --git a/x-pack/plugins/siem/public/app/types.ts b/x-pack/plugins/siem/public/app/types.ts index 1fcbc5ba25f8f..444e0066c3c7b 100644 --- a/x-pack/plugins/siem/public/app/types.ts +++ b/x-pack/plugins/siem/public/app/types.ts @@ -5,7 +5,6 @@ */ import { Reducer, AnyAction, Middleware, Dispatch } from 'redux'; - import { NavTab } from '../common/components/navigation/types'; import { HostsState } from '../hosts/store'; import { NetworkState } from '../network/store'; @@ -15,7 +14,7 @@ import { Immutable } from '../../common/endpoint/types'; import { AlertListState } from '../../common/endpoint_alerts/types'; import { AppAction } from '../common/store/actions'; import { HostState } from '../endpoint_hosts/types'; -import { PolicyDetailsState, PolicyListState } from '../endpoint_policy/types'; +import { ManagementState } from '../management/store/types'; export enum SiemPageName { overview = 'overview', @@ -24,6 +23,7 @@ export enum SiemPageName { detections = 'detections', timelines = 'timelines', case = 'case', + management = 'management', } export type SiemNavTabKey = @@ -32,14 +32,15 @@ export type SiemNavTabKey = | SiemPageName.network | SiemPageName.detections | SiemPageName.timelines - | SiemPageName.case; + | SiemPageName.case + | SiemPageName.management; export type SiemNavTab = Record; export interface SecuritySubPluginStore { initialState: Record; reducer: Record>; - middleware?: Middleware<{}, State, Dispatch>>; + middleware?: Array>>>; } export interface SecuritySubPlugin { @@ -52,8 +53,7 @@ type SecuritySubPluginKeyStore = | 'timeline' | 'hostList' | 'alertList' - | 'policyDetails' - | 'policyList'; + | 'management'; export interface SecuritySubPluginWithStore extends SecuritySubPlugin { store: SecuritySubPluginStore; @@ -67,8 +67,7 @@ export interface SecuritySubPlugins extends SecuritySubPlugin { timeline: TimelineState; alertList: Immutable; hostList: Immutable; - policyDetails: Immutable; - policyList: Immutable; + management: ManagementState; }; reducer: { hosts: Reducer; @@ -76,8 +75,7 @@ export interface SecuritySubPlugins extends SecuritySubPlugin { timeline: Reducer; alertList: ImmutableReducer; hostList: ImmutableReducer; - policyDetails: ImmutableReducer; - policyList: ImmutableReducer; + management: ImmutableReducer; }; middlewares: Array>>>; }; diff --git a/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap index 35a42acf7e1fb..5d077dba447fa 100644 --- a/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap +++ b/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap @@ -21,6 +21,10 @@ exports[`PageView component should display body header custom element 1`] = ` background: none; } +.c0 .endpoint-navTabs { + margin-left: 24px; +} + @@ -112,6 +116,10 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] = background: none; } +.c0 .endpoint-navTabs { + margin-left: 24px; +} + @@ -383,6 +399,10 @@ exports[`PageView component should display only header left 1`] = ` background: none; } +.c0 .endpoint-navTabs { + margin-left: 24px; +} + diff --git a/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx b/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx index ecc480fc97293..759274e3a4ffa 100644 --- a/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx +++ b/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx @@ -14,10 +14,13 @@ import { EuiPageHeader, EuiPageHeaderSection, EuiPageProps, + EuiTab, + EuiTabs, EuiTitle, } from '@elastic/eui'; -import React, { memo, ReactNode } from 'react'; +import React, { memo, MouseEventHandler, ReactNode, useMemo } from 'react'; import styled from 'styled-components'; +import { EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; const StyledEuiPage = styled(EuiPage)` &.endpoint--isListView { @@ -39,6 +42,9 @@ const StyledEuiPage = styled(EuiPage)` background: none; } } + .endpoint-navTabs { + margin-left: ${(props) => props.theme.eui.euiSizeL}; + } `; const isStringOrNumber = /(string|number)/; @@ -74,69 +80,94 @@ export const PageViewBodyHeaderTitle = memo<{ children: ReactNode }>( ); PageViewBodyHeaderTitle.displayName = 'PageViewBodyHeaderTitle'; +export type PageViewProps = EuiPageProps & { + /** + * The type of view + */ + viewType: 'list' | 'details'; + /** + * content to be placed on the left side of the header. If a `string` is used, then it will + * be wrapped with `

`, else it will just be used as is. + */ + headerLeft?: ReactNode; + /** Content for the right side of the header */ + headerRight?: ReactNode; + /** + * body (sub-)header section. If a `string` is used, then it will be wrapped with + * `

` + */ + bodyHeader?: ReactNode; + /** + * The list of tab navigation items + */ + tabs?: Array< + EuiTabProps & { + name: ReactNode; + id: string; + href?: string; + onClick?: MouseEventHandler; + } + >; + children?: ReactNode; +}; + /** * Page View layout for use in Endpoint */ -export const PageView = memo< - EuiPageProps & { - /** - * The type of view - */ - viewType: 'list' | 'details'; - /** - * content to be placed on the left side of the header. If a `string` is used, then it will - * be wrapped with `

`, else it will just be used as is. - */ - headerLeft?: ReactNode; - /** Content for the right side of the header */ - headerRight?: ReactNode; - /** - * body (sub-)header section. If a `string` is used, then it will be wrapped with - * `

` - */ - bodyHeader?: ReactNode; - children?: ReactNode; - } ->(({ viewType, children, headerLeft, headerRight, bodyHeader, ...otherProps }) => { - return ( - - - {(headerLeft || headerRight) && ( - - - {isStringOrNumber.test(typeof headerLeft) ? ( - {headerLeft} - ) : ( - headerLeft - )} - - {headerRight && ( - - {headerRight} - - )} - - )} - - {bodyHeader && ( - - - {isStringOrNumber.test(typeof bodyHeader) ? ( - {bodyHeader} +export const PageView = memo( + ({ viewType, children, headerLeft, headerRight, bodyHeader, tabs, ...otherProps }) => { + const tabComponents = useMemo(() => { + if (!tabs) { + return []; + } + return tabs.map(({ name, id, ...otherEuiTabProps }) => ( + + {name} + + )); + }, [tabs]); + + return ( + + + {(headerLeft || headerRight) && ( + + + {isStringOrNumber.test(typeof headerLeft) ? ( + {headerLeft} ) : ( - bodyHeader + headerLeft )} - - + + {headerRight && ( + + {headerRight} + + )} + )} - {children} - - - - ); -}); + {tabs && {tabComponents}} + + {bodyHeader && ( + + + {isStringOrNumber.test(typeof bodyHeader) ? ( + {bodyHeader} + ) : ( + bodyHeader + )} + + + )} + {children} + + + + ); + } +); PageView.displayName = 'PageView'; diff --git a/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx b/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx index 0f6c5c2e139a7..809f0eeb811f4 100644 --- a/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx @@ -18,6 +18,7 @@ jest.mock('react-router-dom', () => ({ state: '', }), withRouter: () => jest.fn(), + generatePath: jest.fn(), })); // Test will fail because we will to need to mock some core services to make the test work diff --git a/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx index 77636af8bc4a4..8151291679e32 100644 --- a/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx @@ -27,6 +27,7 @@ import { } from './redirect_to_case'; import { DetectionEngineTab } from '../../../alerts/pages/detection_engine/types'; import { TimelineType } from '../../../../common/types/timeline'; +import { RedirectToManagementPage } from './redirect_to_management'; interface LinkToPageProps { match: RouteMatch<{}>; @@ -120,6 +121,10 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToTimelinesPage} path={`${match.url}/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`} /> + )); diff --git a/x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx new file mode 100644 index 0000000000000..595c203993bb7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { RedirectWrapper } from './redirect_wrapper'; +import { SiemPageName } from '../../../app/types'; + +export const RedirectToManagementPage = memo(() => { + return ; +}); + +RedirectToManagementPage.displayName = 'RedirectToManagementPage'; diff --git a/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx b/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx index ff3f9ba0694a9..fd96885e5bc10 100644 --- a/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx @@ -72,6 +72,13 @@ describe('SIEM Navigation', () => { name: 'Cases', urlKey: 'case', }, + management: { + disabled: false, + href: '#/management', + id: 'management', + name: 'Management', + urlKey: 'management', + }, detections: { disabled: false, href: '#/link-to/detections', @@ -111,6 +118,7 @@ describe('SIEM Navigation', () => { pageName: 'hosts', pathName: '/hosts', search: '', + state: undefined, tabName: 'authentications', query: { query: '', language: 'kuery' }, filters: [], @@ -179,6 +187,13 @@ describe('SIEM Navigation', () => { name: 'Hosts', urlKey: 'host', }, + management: { + disabled: false, + href: '#/management', + id: 'management', + name: 'Management', + urlKey: 'management', + }, network: { disabled: false, href: '#/link-to/network', diff --git a/x-pack/plugins/siem/public/common/components/url_state/constants.ts b/x-pack/plugins/siem/public/common/components/url_state/constants.ts index b6ef3c8ccd4e9..1faff2594ce80 100644 --- a/x-pack/plugins/siem/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/siem/public/common/components/url_state/constants.ts @@ -12,6 +12,7 @@ export enum CONSTANTS { filters = 'filters', hostsDetails = 'hosts.details', hostsPage = 'hosts.page', + management = 'management', networkDetails = 'network.details', networkPage = 'network.page', overviewPage = 'overview.page', @@ -22,4 +23,11 @@ export enum CONSTANTS { unknown = 'unknown', } -export type UrlStateType = 'case' | 'detections' | 'host' | 'network' | 'overview' | 'timeline'; +export type UrlStateType = + | 'case' + | 'detections' + | 'host' + | 'network' + | 'overview' + | 'timeline' + | 'management'; diff --git a/x-pack/plugins/siem/public/common/components/url_state/types.ts b/x-pack/plugins/siem/public/common/components/url_state/types.ts index 56578d84e12e4..8881a82e5cd1c 100644 --- a/x-pack/plugins/siem/public/common/components/url_state/types.ts +++ b/x-pack/plugins/siem/public/common/components/url_state/types.ts @@ -8,10 +8,10 @@ import ApolloClient from 'apollo-client'; import * as H from 'history'; import { ActionCreator } from 'typescript-fsa'; import { - IIndexPattern, - Query, Filter, FilterManager, + IIndexPattern, + Query, SavedQueryService, } from 'src/plugins/data/public'; @@ -46,6 +46,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timerange, CONSTANTS.timeline, ], + management: [], network: [ CONSTANTS.appQuery, CONSTANTS.filters, diff --git a/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx index 9a7048efd4d0e..e62f36c2ec782 100644 --- a/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx @@ -15,11 +15,10 @@ import { depsStartMock } from './dependencies_start_mock'; import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils'; import { apolloClientObservable } from '../test_providers'; import { createStore, State, substateMiddlewareFactory } from '../../store'; -import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware'; -import { policyListMiddlewareFactory } from '../../../endpoint_policy/store/policy_list/middleware'; -import { policyDetailsMiddlewareFactory } from '../../../endpoint_policy/store/policy_details/middleware'; +import { hostMiddlewareFactory } from '../../../endpoint_hosts/store'; import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware'; import { AppRootProvider } from './app_root_provider'; +import { managementMiddlewareFactory } from '../../../management/store'; import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -63,18 +62,11 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { (globalState) => globalState.hostList, hostMiddlewareFactory(coreStart, depsStart) ), - substateMiddlewareFactory( - (globalState) => globalState.policyList, - policyListMiddlewareFactory(coreStart, depsStart) - ), - substateMiddlewareFactory( - (globalState) => globalState.policyDetails, - policyDetailsMiddlewareFactory(coreStart, depsStart) - ), substateMiddlewareFactory( (globalState) => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) ), + ...managementMiddlewareFactory(coreStart, depsStart), middlewareSpy.actionSpyMiddleware, ]); diff --git a/x-pack/plugins/siem/public/common/mock/global_state.ts b/x-pack/plugins/siem/public/common/mock/global_state.ts index da49ebe6552f3..c96f67a39dbfe 100644 --- a/x-pack/plugins/siem/public/common/mock/global_state.ts +++ b/x-pack/plugins/siem/public/common/mock/global_state.ts @@ -25,15 +25,13 @@ import { } from '../../../common/constants'; import { networkModel } from '../../network/store'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; -import { initialPolicyListState } from '../../endpoint_policy/store/policy_list/reducer'; import { initialAlertListState } from '../../endpoint_alerts/store/reducer'; -import { initialPolicyDetailsState } from '../../endpoint_policy/store/policy_details/reducer'; import { initialHostListState } from '../../endpoint_hosts/store/reducer'; +import { getManagementInitialState } from '../../management/store'; -const policyList = initialPolicyListState(); const alertList = initialAlertListState(); -const policyDetails = initialPolicyDetailsState(); const hostList = initialHostListState(); +const management = getManagementInitialState(); export const mockGlobalState: State = { app: { @@ -237,6 +235,5 @@ export const mockGlobalState: State = { }, alertList, hostList, - policyList, - policyDetails, + management, }; diff --git a/x-pack/plugins/siem/public/common/mock/utils.ts b/x-pack/plugins/siem/public/common/mock/utils.ts index 68c52e493898f..532637acab767 100644 --- a/x-pack/plugins/siem/public/common/mock/utils.ts +++ b/x-pack/plugins/siem/public/common/mock/utils.ts @@ -9,8 +9,7 @@ import { networkReducer } from '../../network/store'; import { timelineReducer } from '../../timelines/store/timeline/reducer'; import { hostListReducer } from '../../endpoint_hosts/store'; import { alertListReducer } from '../../endpoint_alerts/store'; -import { policyListReducer } from '../../endpoint_policy/store/policy_list'; -import { policyDetailsReducer } from '../../endpoint_policy/store/policy_details'; +import { managementReducer } from '../../management/store'; interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -25,6 +24,5 @@ export const SUB_PLUGINS_REDUCER = { timeline: timelineReducer, hostList: hostListReducer, alertList: alertListReducer, - policyList: policyListReducer, - policyDetails: policyDetailsReducer, + management: managementReducer, }; diff --git a/x-pack/plugins/siem/public/common/store/actions.ts b/x-pack/plugins/siem/public/common/store/actions.ts index a51b075dc7514..58e4e2f363e92 100644 --- a/x-pack/plugins/siem/public/common/store/actions.ts +++ b/x-pack/plugins/siem/public/common/store/actions.ts @@ -6,8 +6,8 @@ import { HostAction } from '../../endpoint_hosts/store/action'; import { AlertAction } from '../../endpoint_alerts/store/action'; -import { PolicyListAction } from '../../endpoint_policy/store/policy_list'; -import { PolicyDetailsAction } from '../../endpoint_policy/store/policy_details'; +import { PolicyListAction } from '../../management/pages/policy/store/policy_list'; +import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; export { appActions } from './app'; export { dragAndDropActions } from './drag_and_drop'; diff --git a/x-pack/plugins/siem/public/common/store/reducer.ts b/x-pack/plugins/siem/public/common/store/reducer.ts index 570e851a3aa5e..e06543b8d7181 100644 --- a/x-pack/plugins/siem/public/common/store/reducer.ts +++ b/x-pack/plugins/siem/public/common/store/reducer.ts @@ -18,14 +18,8 @@ import { EndpointAlertsPluginReducer, } from '../../endpoint_alerts/store'; import { EndpointHostsPluginState, EndpointHostsPluginReducer } from '../../endpoint_hosts/store'; -import { - EndpointPolicyDetailsStatePluginState, - EndpointPolicyDetailsStatePluginReducer, -} from '../../endpoint_policy/store/policy_details'; -import { - EndpointPolicyListStatePluginState, - EndpointPolicyListStatePluginReducer, -} from '../../endpoint_policy/store/policy_list'; + +import { ManagementPluginReducer, ManagementPluginState } from '../../management/store/types'; export interface State extends HostsPluginState, @@ -33,8 +27,7 @@ export interface State TimelinePluginState, EndpointAlertsPluginState, EndpointHostsPluginState, - EndpointPolicyDetailsStatePluginState, - EndpointPolicyListStatePluginState { + ManagementPluginState { app: AppState; dragAndDrop: DragAndDropState; inputs: InputsState; @@ -51,15 +44,14 @@ type SubPluginsInitState = HostsPluginState & TimelinePluginState & EndpointAlertsPluginState & EndpointHostsPluginState & - EndpointPolicyDetailsStatePluginState & - EndpointPolicyListStatePluginState; + ManagementPluginState; + export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & TimelinePluginReducer & EndpointAlertsPluginReducer & EndpointHostsPluginReducer & - EndpointPolicyDetailsStatePluginReducer & - EndpointPolicyListStatePluginReducer; + ManagementPluginReducer; export const createInitialState = (pluginsInitState: SubPluginsInitState): State => ({ ...initialState, diff --git a/x-pack/plugins/siem/public/common/store/types.ts b/x-pack/plugins/siem/public/common/store/types.ts index 0a1010ea87fca..a4bfdeb30b438 100644 --- a/x-pack/plugins/siem/public/common/store/types.ts +++ b/x-pack/plugins/siem/public/common/store/types.ts @@ -61,6 +61,17 @@ export type ImmutableMiddlewareFactory = ( depsStart: Pick ) => ImmutableMiddleware; +/** + * Takes application-standard middleware dependencies + * and returns an array of redux middleware. + * Middleware will be of the `ImmutableMiddleware` variety. Not able to directly + * change actions or state. + */ +export type ImmutableMultipleMiddlewareFactory = ( + coreStart: CoreStart, + depsStart: Pick +) => Array>; + /** * Simple type for a redux selector. */ diff --git a/x-pack/plugins/siem/public/endpoint_alerts/index.ts b/x-pack/plugins/siem/public/endpoint_alerts/index.ts index 8b7e13c118fd0..6380edbde6958 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/index.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/index.ts @@ -22,10 +22,12 @@ export class EndpointAlerts { plugins: StartPlugins ): SecuritySubPluginWithStore<'alertList', Immutable> { const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.alertList, - alertMiddlewareFactory(core, { data, ingestManager }) - ); + const middleware = [ + substateMiddlewareFactory( + (globalState) => globalState.alertList, + alertMiddlewareFactory(core, { data, ingestManager }) + ), + ]; return { routes: getEndpointAlertsRoutes(), diff --git a/x-pack/plugins/siem/public/endpoint_hosts/index.ts b/x-pack/plugins/siem/public/endpoint_hosts/index.ts index c86078ef4b475..1c2649ec5cf91 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/index.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/index.ts @@ -22,10 +22,12 @@ export class EndpointHosts { plugins: StartPlugins ): SecuritySubPluginWithStore<'hostList', Immutable> { const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.hostList, - hostMiddlewareFactory(core, { data, ingestManager }) - ); + const middleware = [ + substateMiddlewareFactory( + (globalState) => globalState.hostList, + hostMiddlewareFactory(core, { data, ingestManager }) + ), + ]; return { routes: getEndpointHostsRoutes(), store: { diff --git a/x-pack/plugins/siem/public/endpoint_policy/details.ts b/x-pack/plugins/siem/public/endpoint_policy/details.ts deleted file mode 100644 index 1375d851067b4..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/details.ts +++ /dev/null @@ -1,41 +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 { SecuritySubPluginWithStore } from '../app/types'; -import { getPolicyDetailsRoutes } from './routes'; -import { PolicyDetailsState } from './types'; -import { Immutable } from '../../common/endpoint/types'; -import { initialPolicyDetailsState, policyDetailsReducer } from './store/policy_details/reducer'; -import { policyDetailsMiddlewareFactory } from './store/policy_details/middleware'; -import { CoreStart } from '../../../../../src/core/public'; -import { StartPlugins } from '../types'; -import { substateMiddlewareFactory } from '../common/store'; - -export class EndpointPolicyDetails { - public setup() {} - - public start( - core: CoreStart, - plugins: StartPlugins - ): SecuritySubPluginWithStore<'policyDetails', Immutable> { - const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.policyDetails, - policyDetailsMiddlewareFactory(core, { data, ingestManager }) - ); - - return { - routes: getPolicyDetailsRoutes(), - store: { - initialState: { - policyDetails: initialPolicyDetailsState(), - }, - reducer: { policyDetails: policyDetailsReducer }, - middleware, - }, - }; - } -} diff --git a/x-pack/plugins/siem/public/endpoint_policy/list.ts b/x-pack/plugins/siem/public/endpoint_policy/list.ts deleted file mode 100644 index 5dad5fac895e0..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/list.ts +++ /dev/null @@ -1,41 +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 { SecuritySubPluginWithStore } from '../app/types'; -import { getPolicyListRoutes } from './routes'; -import { PolicyListState } from './types'; -import { Immutable } from '../../common/endpoint/types'; -import { initialPolicyListState, policyListReducer } from './store/policy_list/reducer'; -import { policyListMiddlewareFactory } from './store/policy_list/middleware'; -import { CoreStart } from '../../../../../src/core/public'; -import { StartPlugins } from '../types'; -import { substateMiddlewareFactory } from '../common/store'; - -export class EndpointPolicyList { - public setup() {} - - public start( - core: CoreStart, - plugins: StartPlugins - ): SecuritySubPluginWithStore<'policyList', Immutable> { - const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.policyList, - policyListMiddlewareFactory(core, { data, ingestManager }) - ); - - return { - routes: getPolicyListRoutes(), - store: { - initialState: { - policyList: initialPolicyListState(), - }, - reducer: { policyList: policyListReducer }, - middleware, - }, - }; - } -} diff --git a/x-pack/plugins/siem/public/endpoint_policy/routes.tsx b/x-pack/plugins/siem/public/endpoint_policy/routes.tsx deleted file mode 100644 index be820f3f2c5dc..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/routes.tsx +++ /dev/null @@ -1,18 +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 { Route } from 'react-router-dom'; - -import { PolicyList, PolicyDetails } from './view'; - -export const getPolicyListRoutes = () => [ - , -]; - -export const getPolicyDetailsRoutes = () => [ - , -]; diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts b/x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts deleted file mode 100644 index 9fadba85c5245..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts +++ /dev/null @@ -1,19 +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 { useSelector } from 'react-redux'; -import { PolicyListState, PolicyDetailsState } from '../types'; -import { State } from '../../common/store'; - -export function usePolicyListSelector(selector: (state: PolicyListState) => TSelected) { - return useSelector((state: State) => selector(state.policyList as PolicyListState)); -} - -export function usePolicyDetailsSelector( - selector: (state: PolicyDetailsState) => TSelected -) { - return useSelector((state: State) => selector(state.policyDetails as PolicyDetailsState)); -} diff --git a/x-pack/plugins/siem/public/management/common/constants.ts b/x-pack/plugins/siem/public/management/common/constants.ts new file mode 100644 index 0000000000000..9ec6817c0bfce --- /dev/null +++ b/x-pack/plugins/siem/public/management/common/constants.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 { SiemPageName } from '../../app/types'; +import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types'; + +// --[ ROUTING ]--------------------------------------------------------------------------- +export const MANAGEMENT_ROUTING_ROOT_PATH = `/:pageName(${SiemPageName.management})`; +export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; +export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`; + +// --[ STORE ]--------------------------------------------------------------------------- +/** The SIEM global store namespace where the management state will be mounted */ +export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace = 'management'; +/** Namespace within the Management state where policy list state is maintained */ +export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList'; +/** Namespace within the Management state where policy details state is maintained */ +export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails'; diff --git a/x-pack/plugins/siem/public/management/common/routing.ts b/x-pack/plugins/siem/public/management/common/routing.ts new file mode 100644 index 0000000000000..e64fcf0c5f68a --- /dev/null +++ b/x-pack/plugins/siem/public/management/common/routing.ts @@ -0,0 +1,70 @@ +/* + * 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 { generatePath } from 'react-router-dom'; +import { + MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + MANAGEMENT_ROUTING_ROOT_PATH, +} from './constants'; +import { ManagementSubTab } from '../types'; +import { SiemPageName } from '../../app/types'; + +export type GetManagementUrlProps = { + /** + * Exclude the URL prefix (everything to the left of where the router was mounted. + * This may be needed when interacting with react-router (ex. to do `history.push()` or + * validations against matched path) + */ + excludePrefix?: boolean; +} & ( + | { name: 'default' } + | { name: 'endpointList' } + | { name: 'policyList' } + | { name: 'policyDetails'; policyId: string } +); + +// Prefix is (almost) everything to the left of where the Router was mounted. In SIEM, since +// we're using Hash router, thats the `#`. +const URL_PREFIX = '#'; + +/** + * Returns a URL string for a given Management page view + * @param props + */ +export const getManagementUrl = (props: GetManagementUrlProps): string => { + let url = props.excludePrefix ? '' : URL_PREFIX; + + switch (props.name) { + case 'default': + url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, { + pageName: SiemPageName.management, + }); + break; + case 'endpointList': + url += generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.endpoints, + }); + break; + case 'policyList': + url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.policies, + }); + break; + case 'policyDetails': + url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.policies, + policyId: props.policyId, + }); + break; + } + + return url; +}; diff --git a/x-pack/plugins/siem/public/management/components/management_page_view.tsx b/x-pack/plugins/siem/public/management/components/management_page_view.tsx new file mode 100644 index 0000000000000..13d8525e15e15 --- /dev/null +++ b/x-pack/plugins/siem/public/management/components/management_page_view.tsx @@ -0,0 +1,37 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useParams } from 'react-router-dom'; +import { PageView, PageViewProps } from '../../common/components/endpoint/page_view'; +import { ManagementSubTab } from '../types'; +import { getManagementUrl } from '..'; + +export const ManagementPageView = memo>((options) => { + const { tabName } = useParams<{ tabName: ManagementSubTab }>(); + const tabs = useMemo((): PageViewProps['tabs'] => { + return [ + { + name: i18n.translate('xpack.siem.managementTabs.endpoints', { + defaultMessage: 'Endpoints', + }), + id: ManagementSubTab.endpoints, + isSelected: tabName === ManagementSubTab.endpoints, + href: getManagementUrl({ name: 'endpointList' }), + }, + { + name: i18n.translate('xpack.siem.managementTabs.policies', { defaultMessage: 'Policies' }), + id: ManagementSubTab.policies, + isSelected: tabName === ManagementSubTab.policies, + href: getManagementUrl({ name: 'policyList' }), + }, + ]; + }, [tabName]); + return ; +}); + +ManagementPageView.displayName = 'ManagementPageView'; diff --git a/x-pack/plugins/siem/public/management/index.ts b/x-pack/plugins/siem/public/management/index.ts new file mode 100644 index 0000000000000..86522df110dfb --- /dev/null +++ b/x-pack/plugins/siem/public/management/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import { managementReducer, getManagementInitialState, managementMiddlewareFactory } from './store'; +import { getManagementRoutes } from './routes'; +import { StartPlugins } from '../types'; +import { MANAGEMENT_STORE_GLOBAL_NAMESPACE } from './common/constants'; +import { SecuritySubPluginWithStore } from '../app/types'; +import { Immutable } from '../../common/endpoint/types'; +import { ManagementStoreGlobalNamespace } from './types'; +import { ManagementState } from './store/types'; + +export { getManagementUrl } from './common/routing'; + +export class Management { + public setup() {} + + public start( + core: CoreStart, + plugins: StartPlugins + ): SecuritySubPluginWithStore> { + return { + routes: getManagementRoutes(), + store: { + initialState: { + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: getManagementInitialState(), + }, + reducer: { + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: managementReducer, + }, + middleware: managementMiddlewareFactory(core, plugins), + }, + }; + } +} diff --git a/x-pack/plugins/siem/public/management/pages/index.tsx b/x-pack/plugins/siem/public/management/pages/index.tsx new file mode 100644 index 0000000000000..aba482db86519 --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/index.tsx @@ -0,0 +1,43 @@ +/* + * 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, { memo } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { PolicyContainer } from './policy'; +import { + MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_ROOT_PATH, +} from '../common/constants'; +import { ManagementPageView } from '../components/management_page_view'; +import { NotFoundPage } from '../../app/404'; + +const TmpEndpoints = () => { + return ( + +

{'Endpoints will go here'}

+ +
+ ); +}; + +export const ManagementContainer = memo(() => { + return ( + + + + } + /> + + + ); +}); + +ManagementContainer.displayName = 'ManagementContainer'; diff --git a/x-pack/plugins/siem/public/management/pages/policy/index.tsx b/x-pack/plugins/siem/public/management/pages/policy/index.tsx new file mode 100644 index 0000000000000..5122bbcd5d55d --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { PolicyDetails, PolicyList } from './view'; +import { + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, +} from '../../common/constants'; +import { NotFoundPage } from '../../../app/404'; + +export const PolicyContainer = memo(() => { + return ( + + + + + + ); +}); + +PolicyContainer.displayName = 'PolicyContainer'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts b/x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts rename to x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts index 44be5ddcc003f..7c67dffb8a663 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UIPolicyConfig } from '../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../common/endpoint/types'; /** * A typed Object.entries() function where the keys and values are typed based on the given object diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts index ceb62a9f9ace9..f729dfbd9a29a 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetAgentStatusResponse } from '../../../../../ingest_manager/common/types/rest_spec'; -import { PolicyData, UIPolicyConfig } from '../../../../common/endpoint/types'; -import { ServerApiError } from '../../../common/types'; +import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec'; +import { PolicyData, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../common/types'; import { PolicyDetailsState } from '../../types'; interface ServerReturnedPolicyDetailsData { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts index 01a824ecc7b8e..469b71854dfcc 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts @@ -9,7 +9,7 @@ import { createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction } from './index'; import { policyConfig } from './selectors'; import { clone } from '../../models/policy_details_config'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; describe('policy details: ', () => { let store: Store; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts similarity index 77% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts index 88f090301cfa3..9ccc47f250e4e 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts @@ -5,9 +5,9 @@ */ import { PolicyDetailsState } from '../../types'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export { policyDetailsMiddlewareFactory } from './middleware'; export { PolicyDetailsAction } from './action'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts similarity index 93% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts index 883d8e780ea67..97cdcac0fcae9 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts @@ -16,9 +16,9 @@ import { sendGetFleetAgentStatusForConfig, sendPutDatasource, } from '../policy_list/services/ingest'; -import { NewPolicyData, PolicyData, Immutable } from '../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; -import { ImmutableMiddlewareFactory } from '../../../common/store'; +import { NewPolicyData, PolicyData, Immutable } from '../../../../../../common/endpoint/types'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { ImmutableMiddlewareFactory } from '../../../../../common/store'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { return { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts index 3c943986a72e4..d2a5c1b7e14a3 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts @@ -5,14 +5,17 @@ */ import { createSelector } from 'reselect'; +import { matchPath } from 'react-router-dom'; import { PolicyDetailsState } from '../../types'; import { Immutable, NewPolicyData, PolicyConfig, UIPolicyConfig, -} from '../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; +} from '../../../../../../common/endpoint/types'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { MANAGEMENT_ROUTING_POLICY_DETAILS_PATH } from '../../../../common/constants'; +import { ManagementRoutePolicyDetailsParams } from '../../../../types'; /** Returns the policy details */ export const policyDetails = (state: Immutable) => state.policyItem; @@ -31,22 +34,24 @@ export const policyDetailsForUpdate: ( /** Returns a boolean of whether the user is on the policy details page or not */ export const isOnPolicyDetailsPage = (state: Immutable) => { - if (state.location) { - const pathnameParts = state.location.pathname.split('/'); - return pathnameParts[1] === 'policy' && pathnameParts[2]; - } else { - return false; - } + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + exact: true, + }) !== null + ); }; /** Returns the policyId from the url */ export const policyIdFromParams: (state: Immutable) => string = createSelector( (state) => state.location, (location: PolicyDetailsState['location']) => { - if (location) { - return location.pathname.split('/')[2]; - } - return ''; + return ( + matchPath(location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + exact: true, + })?.params?.policyId ?? '' + ); } ); diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts similarity index 83% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts index bedbcdae3306f..6866bcbf31f89 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData } from '../../../../common/endpoint/types'; -import { ServerApiError } from '../../../common/types'; +import { PolicyData } from '../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../common/types'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts index 9b56062879583..c796edff8aabc 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts @@ -7,19 +7,24 @@ import { PolicyListState } from '../../types'; import { Store, applyMiddleware, createStore } from 'redux'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../ingest_manager/common'; +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common'; import { policyListReducer, initialPolicyListState } from './reducer'; import { policyListMiddlewareFactory } from './middleware'; import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors'; -import { DepsStartMock, depsStartMock } from '../../../common/mock/endpoint'; +import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint'; import { setPolicyListApiMockImplementation } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; -import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../../../common/store/test_utils'; +import { + createSpyMiddleware, + MiddlewareActionSpyHelper, +} from '../../../../../common/store/test_utils'; +import { getManagementUrl } from '../../../../common/routing'; describe('policy list store concerns', () => { + const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); let fakeCoreStart: ReturnType; let depsStart: DepsStartMock; let store: Store; @@ -57,7 +62,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -70,7 +75,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -84,7 +89,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -112,7 +117,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -132,7 +137,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: searchParams, hash: '', }, diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts similarity index 76% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts index a4f51fcf0ec66..e09f80883d888 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts @@ -5,9 +5,9 @@ */ import { PolicyListState } from '../../types'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export { policyListReducer } from './reducer'; export { PolicyListAction } from './action'; export { policyListMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts similarity index 91% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts index 8602ab8170565..6054ec34b2d01 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts @@ -7,8 +7,8 @@ import { GetPolicyListResponse, PolicyListState } from '../../types'; import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; -import { ImmutableMiddlewareFactory } from '../../../common/store'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableMiddlewareFactory } from '../../../../../common/store'; +import { Immutable } from '../../../../../../common/endpoint/types'; export const policyListMiddlewareFactory: ImmutableMiddlewareFactory> = ( coreStart diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts similarity index 89% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts index 80e890602c921..028e46936b293 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts @@ -6,9 +6,9 @@ import { PolicyListState } from '../../types'; import { isOnPolicyListPage } from './selectors'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export const initialPolicyListState = (): PolicyListState => { return { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts index cd6230a6ed3be..c900ceb186f69 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts @@ -6,8 +6,10 @@ import { createSelector } from 'reselect'; import { parse } from 'query-string'; +import { matchPath } from 'react-router-dom'; import { PolicyListState, PolicyListUrlSearchParams } from '../../types'; -import { Immutable } from '../../../../common/endpoint/types'; +import { Immutable } from '../../../../../../common/endpoint/types'; +import { MANAGEMENT_ROUTING_POLICIES_PATH } from '../../../../common/constants'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -24,7 +26,12 @@ export const selectIsLoading = (state: Immutable) => state.isLo export const selectApiError = (state: Immutable) => state.apiError; export const isOnPolicyListPage = (state: Immutable) => { - return state.location?.pathname === '/policy'; + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICIES_PATH, + exact: true, + }) !== null + ); }; const routeLocation = (state: Immutable) => state.location; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts similarity index 94% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts index df61bbe893c58..cbbc5c3c6fdbe 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -5,8 +5,8 @@ */ import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; -import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; +import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; describe('ingest service', () => { let http: ReturnType; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts similarity index 95% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts index 312a3f7491ab2..db482e2a6bdb6 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -9,9 +9,9 @@ import { GetDatasourcesRequest, GetAgentStatusResponse, DATASOURCE_SAVED_OBJECT_TYPE, -} from '../../../../../../ingest_manager/common'; +} from '../../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; -import { NewPolicyData } from '../../../../../common/endpoint/types'; +import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts similarity index 93% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b8fac21b76a26..2c495202dc75b 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -6,7 +6,7 @@ import { HttpStart } from 'kibana/public'; import { INGEST_API_DATASOURCES } from './services/ingest'; -import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; const generator = new EndpointDocGenerator('policy-list'); diff --git a/x-pack/plugins/siem/public/endpoint_policy/types.ts b/x-pack/plugins/siem/public/management/pages/policy/types.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/types.ts rename to x-pack/plugins/siem/public/management/pages/policy/types.ts index ba42140589789..f8cc0d5cd0508 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/types.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/types.ts @@ -10,14 +10,14 @@ import { MalwareFields, UIPolicyConfig, AppLocation, -} from '../../common/endpoint/types'; -import { ServerApiError } from '../common/types'; +} from '../../../../common/endpoint/types'; +import { ServerApiError } from '../../../common/types'; import { GetAgentStatusResponse, GetDatasourcesResponse, GetOneDatasourceResponse, UpdateDatasourceResponse, -} from '../../../ingest_manager/common'; +} from '../../../../../ingest_manager/common'; /** * Policy list store state diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/agents_summary.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/agents_summary.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/agents_summary.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/agents_summary.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/index.ts b/x-pack/plugins/siem/public/management/pages/policy/view/index.ts similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/view/index.ts diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx index 5d736da4e5635..01e12e6c767a6 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx @@ -8,12 +8,20 @@ import React from 'react'; import { mount } from 'enzyme'; import { PolicyDetails } from './policy_details'; -import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; -import { createAppRootMockRenderer } from '../../common/mock/endpoint'; +import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { getManagementUrl } from '../../../common/routing'; describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType['find']>; + const policyDetailsPathUrl = getManagementUrl({ + name: 'policyDetails', + policyId: '1', + excludePrefix: true, + }); + const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); + const policyListPathUrlWithPrefix = getManagementUrl({ name: 'policyList' }); const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); const generator = new EndpointDocGenerator(); const { history, AppWrapper, coreStart } = createAppRootMockRenderer(); @@ -33,7 +41,7 @@ describe('Policy Details', () => { describe('when displayed with invalid id', () => { beforeEach(() => { http.get.mockReturnValue(Promise.reject(new Error('policy not found'))); - history.push('/policy/1'); + history.push(policyDetailsPathUrl); policyView = render(); }); @@ -77,7 +85,7 @@ describe('Policy Details', () => { return Promise.reject(new Error('unknown API call!')); }); - history.push('/policy/1'); + history.push(policyDetailsPathUrl); policyView = render(); }); @@ -89,7 +97,7 @@ describe('Policy Details', () => { const backToListButton = pageHeaderLeft.find('EuiButtonEmpty'); expect(backToListButton.prop('iconType')).toBe('arrowLeft'); - expect(backToListButton.prop('href')).toBe('/mock/app/endpoint/policy'); + expect(backToListButton.prop('href')).toBe(policyListPathUrlWithPrefix); expect(backToListButton.text()).toBe('Back to policy list'); const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]'); @@ -101,9 +109,9 @@ describe('Policy Details', () => { const backToListButton = policyView.find( 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' ); - expect(history.location.pathname).toEqual('/policy/1'); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); backToListButton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual('/policy'); + expect(history.location.pathname).toEqual(policyListPathUrl); }); it('should display agent stats', async () => { await asyncActions; @@ -130,9 +138,9 @@ describe('Policy Details', () => { const cancelbutton = policyView.find( 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' ); - expect(history.location.pathname).toEqual('/policy/1'); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); cancelbutton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual('/policy'); + expect(history.location.pathname).toEqual(policyListPathUrl); }); it('should display save button', async () => { await asyncActions; diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx index c928a374502a5..bddbd378f9427 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx @@ -28,18 +28,21 @@ import { isLoading, apiError, } from '../store/policy_details/selectors'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; -import { AppAction } from '../../common/store/actions'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { PageView, PageViewHeaderTitle } from '../../common/components/endpoint/page_view'; +import { AppAction } from '../../../../common/store/actions'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { getManagementUrl } from '../../../common/routing'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); - const { notifications, services } = useKibana(); + const { notifications } = useKibana(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -81,7 +84,9 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyName, policyUpdateStatus]); - const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); + const handleBackToListOnClick = useNavigateByRouterEventHandler( + getManagementUrl({ name: 'policyList', excludePrefix: true }) + ); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); @@ -103,7 +108,7 @@ export const PolicyDetails = React.memo(() => { // Else, if we have an error, then show error on the page. if (!policyItem) { return ( - + {isPolicyLoading ? ( ) : policyApiError ? ( @@ -111,7 +116,8 @@ export const PolicyDetails = React.memo(() => { {policyApiError?.message} ) : null} - + + ); } @@ -122,7 +128,7 @@ export const PolicyDetails = React.memo(() => { iconType="arrowLeft" contentProps={{ style: { paddingLeft: '0' } }} onClick={handleBackToListOnClick} - href={`${services.http.basePath.get()}/app/endpoint/policy`} + href={getManagementUrl({ name: 'policyList' })} > { onConfirm={handleSaveConfirmation} /> )} - { - + + ); }); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/config_form.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/config_form.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/config_form.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/config_form.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx similarity index 95% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx index fe062526c8d3c..e5f3b2c7e8b7e 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx @@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { policyConfig } from '../../../store/policy_details/selectors'; import { PolicyDetailsAction } from '../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const EventsCheckbox = React.memo(function ({ name, diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/index.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/index.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/index.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx similarity index 97% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx index ff7296ad5a44e..a4f5bb83b6ef3 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx similarity index 97% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx index 1c6d96e555cef..af28a4803518c 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx similarity index 98% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx index 8add5bed23a29..feddf78cd9c5f 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig, Immutable } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx similarity index 98% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 69c0faf6e800e..e60713ca32d5b 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -11,7 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer, htmlIdGenerator } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Immutable, ProtectionModes } from '../../../../../common/endpoint/types'; +import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts new file mode 100644 index 0000000000000..97436064eebe2 --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector } from 'react-redux'; +import { PolicyListState, PolicyDetailsState } from '../types'; +import { State } from '../../../../common/store'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../../../common/constants'; + +/** + * Narrows global state down to the PolicyListState before calling the provided Policy List Selector + * @param selector + */ +export function usePolicyListSelector(selector: (state: PolicyListState) => TSelected) { + return useSelector((state: State) => { + return selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][ + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE + ] as PolicyListState + ); + }); +} + +/** + * Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector + * @param selector + */ +export function usePolicyDetailsSelector( + selector: (state: PolicyDetailsState) => TSelected +) { + return useSelector((state: State) => + selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][ + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE + ] as PolicyDetailsState + ) + ); +} diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx similarity index 84% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx index a9aea57239ed1..3a8004aa2ec6d 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx @@ -20,11 +20,13 @@ import { } from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { Immutable, PolicyData } from '../../../common/endpoint/types'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { PageView } from '../../common/components/endpoint/page_view'; -import { LinkToApp } from '../../common/components/endpoint/link_to_app'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { Immutable, PolicyData } from '../../../../../common/endpoint/types'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { getManagementUrl } from '../../../common/routing'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -93,14 +95,13 @@ export const PolicyList = React.memo(() => { }), // eslint-disable-next-line react/display-name render: (value: string, item: Immutable) => { - const routeUri = `/policy/${item.id}`; - return ( - - ); + const routePath = getManagementUrl({ + name: 'policyDetails', + policyId: item.id, + excludePrefix: true, + }); + const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id }); + return ; }, truncateText: true, }, @@ -150,7 +151,7 @@ export const PolicyList = React.memo(() => { ); return ( - { onChange={handleTableChange} data-test-subj="policyTable" /> - + + ); }); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts b/x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts similarity index 92% rename from x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts rename to x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts index dd74980add7e0..6a3aecb4a6503 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts @@ -5,7 +5,7 @@ */ import styled from 'styled-components'; -import { EuiTheme } from '../../../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../../../../../../legacy/common/eui_styled_components'; type SpacingOptions = keyof EuiTheme['eui']['spacerSizes']; diff --git a/x-pack/plugins/siem/public/management/routes.tsx b/x-pack/plugins/siem/public/management/routes.tsx new file mode 100644 index 0000000000000..fbcea37c76962 --- /dev/null +++ b/x-pack/plugins/siem/public/management/routes.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Route } from 'react-router-dom'; +import { ManagementContainer } from './pages'; +import { MANAGEMENT_ROUTING_ROOT_PATH } from './common/constants'; + +/** + * Returns the React Router Routes for the management area + */ +export const getManagementRoutes = () => [ + // Mounts the Management interface on `/management` + , +]; diff --git a/x-pack/plugins/siem/public/management/store/index.ts b/x-pack/plugins/siem/public/management/store/index.ts new file mode 100644 index 0000000000000..50049f9828082 --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { managementReducer, getManagementInitialState } from './reducer'; +export { managementMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/management/store/middleware.ts b/x-pack/plugins/siem/public/management/store/middleware.ts new file mode 100644 index 0000000000000..f73736e04a5b7 --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/middleware.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 { ImmutableMultipleMiddlewareFactory, substateMiddlewareFactory } from '../../common/store'; +import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; +import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; + +// @ts-ignore +export const managementMiddlewareFactory: ImmutableMultipleMiddlewareFactory = ( + coreStart, + depsStart +) => { + return [ + substateMiddlewareFactory( + (globalState) => + globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE], + policyListMiddlewareFactory(coreStart, depsStart) + ), + substateMiddlewareFactory( + (globalState) => + globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE], + policyDetailsMiddlewareFactory(coreStart, depsStart) + ), + ]; +}; diff --git a/x-pack/plugins/siem/public/management/store/reducer.ts b/x-pack/plugins/siem/public/management/store/reducer.ts new file mode 100644 index 0000000000000..ba7927684ad3d --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/reducer.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers as reduxCombineReducers } from 'redux'; +import { + initialPolicyDetailsState, + policyDetailsReducer, +} from '../pages/policy/store/policy_details/reducer'; +import { + initialPolicyListState, + policyListReducer, +} from '../pages/policy/store/policy_list/reducer'; +import { + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; +import { ImmutableCombineReducers } from '../../common/store'; +import { AppAction } from '../../common/store/actions'; +import { ManagementState } from './types'; + +// Change the type of `combinerReducers` locally +const combineReducers: ImmutableCombineReducers = reduxCombineReducers; + +/** + * Returns the initial state of the store for the SIEM Management section + */ +export const getManagementInitialState = (): ManagementState => { + return { + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), + }; +}; + +/** + * Redux store reducer for the SIEM Management section + */ +export const managementReducer = combineReducers({ + // @ts-ignore + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, + // @ts-ignore + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, +}); diff --git a/x-pack/plugins/siem/public/management/store/types.ts b/x-pack/plugins/siem/public/management/store/types.ts new file mode 100644 index 0000000000000..884724982fa8f --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Immutable } from '../../../common/endpoint/types'; +import { PolicyDetailsState, PolicyListState } from '../pages/policy/types'; +import { ImmutableReducer } from '../../common/store'; +import { AppAction } from '../../common/store/actions'; + +/** + * Redux store state for the Management section + */ +export interface ManagementState { + policyDetails: Immutable; + policyList: Immutable; +} + +export interface ManagementPluginState { + management: ManagementState; +} + +export interface ManagementPluginReducer { + management: ImmutableReducer; +} diff --git a/x-pack/plugins/siem/public/management/types.ts b/x-pack/plugins/siem/public/management/types.ts new file mode 100644 index 0000000000000..5ee16bcd434e3 --- /dev/null +++ b/x-pack/plugins/siem/public/management/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SiemPageName } from '../app/types'; + +/** + * The type for the management store global namespace. Used mostly internally to reference + * the type while defining more complex interfaces/types + */ +export type ManagementStoreGlobalNamespace = 'management'; + +/** + * The management list of sub-tabs. Changes to these will impact the Router routes. + */ +export enum ManagementSubTab { + endpoints = 'endpoints', + policies = 'policy', +} + +/** + * The URL route params for the Management Policy List section + */ +export interface ManagementRoutePolicyListParams { + pageName: SiemPageName.management; + tabName: ManagementSubTab.policies; +} + +/** + * The URL route params for the Management Policy Details section + */ +export interface ManagementRoutePolicyDetailsParams extends ManagementRoutePolicyListParams { + policyId: string; +} diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index 9bea776220720..4b8fc078fc016 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -64,13 +64,8 @@ export class Plugin implements IPlugin Date: Fri, 29 May 2020 10:26:00 -0400 Subject: [PATCH 16/38] Refactoring nav links and header components (#66685) Co-authored-by: spalger --- ...n-core-public.chromenavlink.euiicontype.md | 2 +- ...a-plugin-core-public.chromenavlink.href.md | 13 + ...kibana-plugin-core-public.chromenavlink.md | 3 +- ...re-public.chromenavlinkupdateablefields.md | 2 +- ...in-plugins-data-public.querystringinput.md | 2 +- ...na-plugin-plugins-data-public.searchbar.md | 4 +- src/core/public/chrome/chrome_service.tsx | 50 +- src/core/public/chrome/nav_links/nav_link.ts | 14 +- .../public/chrome/nav_links/to_nav_link.ts | 27 +- .../recently_accessed_service.ts | 2 +- .../collapsible_nav.test.tsx.snap | 1010 +- .../header/__snapshots__/header.test.tsx.snap | 14297 ++++++++++++++++ .../header_breadcrumbs.test.tsx.snap | 2 + src/core/public/chrome/ui/header/_index.scss | 2 - ...lapsible_nav.scss => collapsible_nav.scss} | 0 .../chrome/ui/header/collapsible_nav.test.tsx | 98 +- .../chrome/ui/header/collapsible_nav.tsx | 115 +- .../public/chrome/ui/header/header.test.tsx | 106 + src/core/public/chrome/ui/header/header.tsx | 299 +- .../ui/header/header_breadcrumbs.test.tsx | 19 +- .../chrome/ui/header/header_breadcrumbs.tsx | 96 +- .../public/chrome/ui/header/header_logo.tsx | 23 +- .../chrome/ui/header/header_nav_controls.tsx | 39 +- .../public/chrome/ui/header/nav_drawer.tsx | 42 +- src/core/public/chrome/ui/header/nav_link.tsx | 129 +- src/core/public/public.api.md | 3 +- src/core/server/legacy/types.ts | 2 +- src/plugins/data/public/public.api.md | 6 +- 28 files changed, 15657 insertions(+), 750 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md create mode 100644 src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap rename src/core/public/chrome/ui/header/{_collapsible_nav.scss => collapsible_nav.scss} (100%) create mode 100644 src/core/public/chrome/ui/header/header.test.tsx diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md index fe95cb38cd97c..e30e8262f40b2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md @@ -4,7 +4,7 @@ ## ChromeNavLink.euiIconType property -A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. +A EUI iconType that will be used for the app's icon. This icon takes precedence over the `icon` property. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md new file mode 100644 index 0000000000000..a8af0c997ca78 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [href](./kibana-plugin-core-public.chromenavlink.href.md) + +## ChromeNavLink.href property + +Settled state between `url`, `baseUrl`, and `active` + +Signature: + +```typescript +readonly href?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index a9fabb38df869..0349e865bff97 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -20,8 +20,9 @@ export interface ChromeNavLink | [category](./kibana-plugin-core-public.chromenavlink.category.md) | AppCategory | The category the app lives in | | [disabled](./kibana-plugin-core-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | | [disableSubUrlTracking](./kibana-plugin-core-public.chromenavlink.disablesuburltracking.md) | boolean | A flag that tells legacy chrome to ignore the link when tracking sub-urls | -| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | | [hidden](./kibana-plugin-core-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | +| [href](./kibana-plugin-core-public.chromenavlink.href.md) | string | Settled state between url, baseUrl, and active | | [icon](./kibana-plugin-core-public.chromenavlink.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | | [linkToLastSubUrl](./kibana-plugin-core-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md index 7f6dc7e0d5640..bd5a1399cded7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type ChromeNavLinkUpdateableFields = Partial>; +export declare type ChromeNavLinkUpdateableFields = Partial>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index 58690300b3bd6..85eb4825bc2e3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index b015ebfcbaada..fc141b8c89c18 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index fc7e78f209022..67cd43f0647e4 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -36,7 +36,7 @@ import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; -import { Header, LoadingIndicator } from './ui'; +import { Header } from './ui'; import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -214,31 +214,29 @@ export class ChromeService { docTitle, getHeaderComponent: () => ( - - -
- +
), setAppTitle: (appTitle: string) => appTitle$.next(appTitle), diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index fb2972735c2b7..55b5c80526bab 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -62,7 +62,7 @@ export interface ChromeNavLink { /** * A EUI iconType that will be used for the app's icon. This icon - * takes precendence over the `icon` property. + * takes precedence over the `icon` property. */ readonly euiIconType?: string; @@ -72,6 +72,14 @@ export interface ChromeNavLink { */ readonly icon?: string; + /** + * Settled state between `url`, `baseUrl`, and `active` + * + * @internalRemarks + * This should be required once legacy apps are gone. + */ + readonly href?: string; + /** LEGACY FIELDS */ /** @@ -144,7 +152,7 @@ export interface ChromeNavLink { /** @public */ export type ChromeNavLinkUpdateableFields = Partial< - Pick + Pick >; export class NavLinkWrapper { @@ -162,7 +170,7 @@ export class NavLinkWrapper { public update(newProps: ChromeNavLinkUpdateableFields) { // Enforce limited properties at runtime for JS code - newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase']); + newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase', 'href']); return new NavLinkWrapper({ ...this.properties, ...newProps }); } } diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index f79b1df77f8e1..24744fe53c82c 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -24,7 +24,12 @@ import { appendAppPath } from '../../application/utils'; export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; - const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!); + const relativeBaseUrl = isLegacyApp(app) + ? basePath.prepend(app.appUrl) + : basePath.prepend(app.appRoute!); + const url = relativeToAbsolute(appendAppPath(relativeBaseUrl, app.defaultPath)); + const baseUrl = relativeToAbsolute(relativeBaseUrl); + return new NavLinkWrapper({ ...app, hidden: useAppStatus @@ -32,17 +37,27 @@ export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWra : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, legacy: isLegacyApp(app), - baseUrl: relativeToAbsolute(baseUrl), + baseUrl, ...(isLegacyApp(app) - ? {} + ? { + href: url && !url.startsWith(app.subUrlBase!) ? url : baseUrl, + } : { - url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)), + href: url, + url, }), }); } -function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls +/** + * @param {string} url - a relative or root relative url. If a relative path is given then the + * absolute url returned will depend on the current page where this function is called from. For example + * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get + * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that + * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". + * @return {string} the relative url transformed into an absolute url + */ +export function relativeToAbsolute(url: string) { const a = document.createElement('a'); a.setAttribute('href', url); return a.href; diff --git a/src/core/public/chrome/recently_accessed/recently_accessed_service.ts b/src/core/public/chrome/recently_accessed/recently_accessed_service.ts index 27dbc288d18cb..86c7f3a1ef765 100644 --- a/src/core/public/chrome/recently_accessed/recently_accessed_service.ts +++ b/src/core/public/chrome/recently_accessed/recently_accessed_service.ts @@ -76,7 +76,7 @@ export interface ChromeRecentlyAccessed { * * @param link a relative URL to the resource (not including the {@link HttpStart.basePath | `http.basePath`}) * @param label the label to display in the UI - * @param id a unique string used to de-duplicate the recently accessed llist. + * @param id a unique string used to de-duplicate the recently accessed list. */ add(link: string, label: string, id: string): void; diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 866ea5f45d986..f5b17f8d214e9 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -2,140 +2,295 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
@@ -376,7 +531,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-label="recent 2" class="euiListGroupItem__button" data-test-subj="collapsibleNavAppLink--recent" - href="recent 2" + href="http://localhost/recent%202" rel="noreferrer" title="recent 2" > @@ -465,7 +620,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` style="max-width: none;" >
  • @@ -1023,7 +1179,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-label="recent 2" class="euiListGroupItem__button" data-test-subj="collapsibleNavAppLink--recent" - href="recent 2" + href="http://localhost/recent%202" rel="noreferrer" title="recent 2" > @@ -1112,7 +1268,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` style="max-width: none;" >
  • + +
    +
    +
    + +
  • +`; + +exports[`Header renders 2`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + +
    +
    +`; + +exports[`Header renders 3`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + + + +
    + + + + +
    + + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + +
    + +
    +
    +
    + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
    +
    + +
    + + + + + +
    +
    +`; + +exports[`Header renders 4`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + + +
    + + + +
    +
    +
    + +
    + + + + +
    + + + + +
    + + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + +
    +
    +`; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap index d089019915686..fdaa17c279a10 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -5,6 +5,7 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = aria-current="page" className="euiBreadcrumb euiBreadcrumb--last" data-test-subj="breadcrumb first last" + title="First" > First @@ -39,6 +40,7 @@ Array [ aria-current="page" className="euiBreadcrumb euiBreadcrumb--last" data-test-subj="breadcrumb last" + title="Second" > Second , diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 1b0438d748ff0..5c5e7f18b60a4 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,5 +1,3 @@ -@import './collapsible_nav'; - // TODO #64541 // Delete this block .chrHeaderWrapper:not(.headerWrapper) { diff --git a/src/core/public/chrome/ui/header/_collapsible_nav.scss b/src/core/public/chrome/ui/header/collapsible_nav.scss similarity index 100% rename from src/core/public/chrome/ui/header/_collapsible_nav.scss rename to src/core/public/chrome/ui/header/collapsible_nav.scss diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 527f0df598c7c..5a734d55445a2 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -19,11 +19,13 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; -import { CollapsibleNav } from './collapsible_nav'; -import { DEFAULT_APP_CATEGORIES } from '../../..'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { NavLink, RecentNavLink } from './nav_link'; +import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; +import { CollapsibleNav } from './collapsible_nav'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -31,40 +33,42 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; -function mockLink({ label = 'discover', category, onClick }: Partial) { +function mockLink({ title = 'discover', category }: Partial) { return { - key: label, - label, - href: label, - isActive: true, - onClick: onClick || (() => {}), + title, category, - 'data-test-subj': label, + id: title, + href: title, + baseUrl: '/', + legacy: false, + isActive: true, + 'data-test-subj': title, }; } -function mockRecentNavLink({ label = 'recent', onClick }: Partial) { +function mockRecentNavLink({ label = 'recent' }: Partial) { return { - href: label, label, - title: label, - 'aria-label': label, - onClick, + link: label, + id: label, }; } function mockProps() { return { - id: 'collapsible-nav', - homeHref: '/', + appId$: new BehaviorSubject('test'), + basePath: httpServiceMock.createSetupContract({ basePath: '/test' }).basePath, + id: 'collapsibe-nav', isLocked: false, isOpen: false, - navLinks: [], - recentNavLinks: [], + homeHref: '/', + legacyMode: false, + navLinks$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), storage: new StubBrowserStorage(), - onIsOpenUpdate: () => {}, onIsLockedUpdate: () => {}, - navigateToApp: () => {}, + closeNav: () => {}, + navigateToApp: () => Promise.resolve(), }; } @@ -103,14 +107,14 @@ describe('CollapsibleNav', () => { it('renders links grouped by category', () => { // just a test of category functionality, categories are not accurate const navLinks = [ - mockLink({ label: 'discover', category: kibana }), - mockLink({ label: 'siem', category: security }), - mockLink({ label: 'metrics', category: observability }), - mockLink({ label: 'monitoring', category: management }), - mockLink({ label: 'visualize', category: kibana }), - mockLink({ label: 'dashboard', category: kibana }), - mockLink({ label: 'canvas' }), // links should be able to be rendered top level as well - mockLink({ label: 'logs', category: observability }), + mockLink({ title: 'discover', category: kibana }), + mockLink({ title: 'siem', category: security }), + mockLink({ title: 'metrics', category: observability }), + mockLink({ title: 'monitoring', category: management }), + mockLink({ title: 'visualize', category: kibana }), + mockLink({ title: 'dashboard', category: kibana }), + mockLink({ title: 'canvas' }), // links should be able to be rendered top level as well + mockLink({ title: 'logs', category: observability }), ]; const recentNavLinks = [ mockRecentNavLink({ label: 'recent 1' }), @@ -120,8 +124,8 @@ describe('CollapsibleNav', () => { ); expect(component).toMatchSnapshot(); @@ -134,8 +138,8 @@ describe('CollapsibleNav', () => { ); expectShownNavLinksCount(component, 3); @@ -149,32 +153,34 @@ describe('CollapsibleNav', () => { }); it('closes the nav after clicking a link', () => { - const onClick = sinon.spy(); - const onIsOpenUpdate = sinon.spy(); - const navLinks = [mockLink({ category: kibana, onClick })]; - const recentNavLinks = [mockRecentNavLink({ onClick })]; + const onClose = sinon.spy(); + const navLinks = [mockLink({ category: kibana }), mockLink({ title: 'categoryless' })]; + const recentNavLinks = [mockRecentNavLink({})]; const component = mount( ); component.setProps({ - onIsOpenUpdate: (isOpen: boolean) => { - component.setProps({ isOpen }); - onIsOpenUpdate(); + closeNav: () => { + component.setProps({ isOpen: false }); + onClose(); }, }); component.find('[data-test-subj="collapsibleNavGroup-recentlyViewed"] a').simulate('click'); - expect(onClick.callCount).toEqual(1); - expect(onIsOpenUpdate.callCount).toEqual(1); + expect(onClose.callCount).toEqual(1); expectNavIsClosed(component); component.setProps({ isOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-kibana"] a').simulate('click'); - expect(onClick.callCount).toEqual(2); - expect(onIsOpenUpdate.callCount).toEqual(2); + expect(onClose.callCount).toEqual(2); + expectNavIsClosed(component); + component.setProps({ isOpen: true }); + component.find('[data-test-subj="collapsibleNavGroup-noCategory"] a').simulate('click'); + expect(onClose.callCount).toEqual(3); + expectNavIsClosed(component); }); }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 8bca42db23517..9494e22920de8 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -17,6 +17,7 @@ * under the License. */ +import './collapsible_nav.scss'; import { EuiCollapsibleNav, EuiCollapsibleNavGroup, @@ -30,11 +31,16 @@ import { import { i18n } from '@kbn/i18n'; import { groupBy, sortBy } from 'lodash'; import React, { useRef } from 'react'; +import { useObservable } from 'react-use'; +import * as Rx from 'rxjs'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { NavLink, RecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink } from './nav_link'; -function getAllCategories(allCategorizedLinks: Record) { +function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; for (const [key, value] of Object.entries(allCategorizedLinks)) { @@ -45,7 +51,7 @@ function getAllCategories(allCategorizedLinks: Record) { } function getOrderedCategories( - mainCategories: Record, + mainCategories: Record, categoryDictionary: ReturnType ) { return sortBy( @@ -69,35 +75,53 @@ function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { } interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; + id: string; isLocked: boolean; isOpen: boolean; - navLinks: NavLink[]; - recentNavLinks: RecentNavLink[]; homeHref: string; - id: string; + legacyMode: boolean; + navLinks$: Rx.Observable; + recentlyAccessed$: Rx.Observable; storage?: Storage; onIsLockedUpdate: OnIsLockedUpdate; - onIsOpenUpdate: (isOpen?: boolean) => void; - navigateToApp: (appId: string) => void; + closeNav: () => void; + navigateToApp: InternalApplicationStart['navigateToApp']; } export function CollapsibleNav({ + basePath, + id, isLocked, isOpen, - navLinks, - recentNavLinks, - onIsLockedUpdate, - onIsOpenUpdate, homeHref, - id, - navigateToApp, + legacyMode, storage = window.localStorage, + onIsLockedUpdate, + closeNav, + navigateToApp, + ...observables }: Props) { + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); + const appId = useObservable(observables.appId$, ''); const lockRef = useRef(null); const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); + const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => { + return createEuiListItem({ + link, + legacyMode, + appId, + dataTestSubj: 'collapsibleNavAppLink', + navigateToApp, + onClick: closeNav, + ...(needsIcon && { basePath }), + }); + }; return ( {/* Pinned items */} @@ -127,7 +151,7 @@ export function CollapsibleNav({ iconType: 'home', href: homeHref, onClick: (event: React.MouseEvent) => { - onIsOpenUpdate(false); + closeNav(); if ( event.isDefaultPrevented() || event.altKey || @@ -159,21 +183,22 @@ export function CollapsibleNav({ onToggle={(isCategoryOpen) => setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} data-test-subj="collapsibleNavGroup-recentlyViewed" > - {recentNavLinks.length > 0 ? ( + {recentlyAccessed.length > 0 ? ( {}, ...link }) => ({ - 'data-test-subj': 'collapsibleNavAppLink--recent', - onClick: (e: React.MouseEvent) => { - onIsOpenUpdate(false); - onClick(e); - }, - ...link, - }))} + listItems={recentlyAccessed.map((link) => { + // TODO #64541 + // Can remove icon from recent links completely + const { iconType, ...hydratedLink } = createRecentNavLink(link, navLinks, basePath); + + return { + ...hydratedLink, + 'data-test-subj': 'collapsibleNavAppLink--recent', + onClick: closeNav, + }; + })} maxWidth="none" color="subdued" gutterSize="none" @@ -195,21 +220,8 @@ export function CollapsibleNav({ {/* Kibana, Observability, Security, and Management sections */} - {orderedCategories.map((categoryName, i) => { + {orderedCategories.map((categoryName) => { const category = categoryDictionary[categoryName]!; - const links = allCategorizedLinks[categoryName].map( - ({ label, href, isActive, isDisabled, onClick }) => ({ - label, - href, - isActive, - isDisabled, - 'data-test-subj': 'collapsibleNavAppLink', - onClick: (e: React.MouseEvent) => { - onIsOpenUpdate(false); - onClick(e); - }, - }) - ); return ( readyForEUI(link))} maxWidth="none" color="subdued" gutterSize="none" @@ -237,23 +249,10 @@ export function CollapsibleNav({ })} {/* Things with no category (largely for custom plugins) */} - {unknowns.map(({ label, href, icon, isActive, isDisabled, onClick }, i) => ( - + {unknowns.map((link, i) => ( + - ) => { - onIsOpenUpdate(false); - onClick(e); - }} - /> + ))} diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx new file mode 100644 index 0000000000000..13e1f6f086ae2 --- /dev/null +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -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 React from 'react'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { NavType } from '.'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { applicationServiceMock } from '../../../mocks'; +import { Header } from './header'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +function mockProps() { + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + const application = applicationServiceMock.createInternalStartContract(); + + return { + application, + kibanaVersion: '1.0.0', + appTitle$: new BehaviorSubject('test'), + badge$: new BehaviorSubject(undefined), + breadcrumbs$: new BehaviorSubject([]), + homeHref: '/', + isVisible$: new BehaviorSubject(true), + kibanaDocLink: '/docs', + navLinks$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), + forceAppSwitcherNavigation$: new BehaviorSubject(false), + helpExtension$: new BehaviorSubject(undefined), + helpSupportUrl$: new BehaviorSubject(''), + legacyMode: false, + navControlsLeft$: new BehaviorSubject([]), + navControlsRight$: new BehaviorSubject([]), + basePath: http.basePath, + isLocked$: new BehaviorSubject(false), + navType$: new BehaviorSubject('modern' as NavType), + loadingCount$: new BehaviorSubject(0), + onIsLockedUpdate: () => {}, + }; +} + +describe('Header', () => { + beforeAll(() => { + Object.defineProperty(window, 'localStorage', { + value: new StubBrowserStorage(), + }); + }); + + it('renders', () => { + const isVisible$ = new BehaviorSubject(false); + const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); + const isLocked$ = new BehaviorSubject(false); + const navType$ = new BehaviorSubject('modern' as NavType); + const navLinks$ = new BehaviorSubject([ + { id: 'kibana', title: 'kibana', baseUrl: '', legacy: false }, + ]); + const recentlyAccessed$ = new BehaviorSubject([ + { link: '', label: 'dashboard', id: 'dashboard' }, + ]); + const component = mountWithIntl( +
    + ); + expect(component).toMatchSnapshot(); + + act(() => isVisible$.next(true)); + component.update(); + expect(component).toMatchSnapshot(); + + act(() => isLocked$.next(true)); + component.update(); + expect(component).toMatchSnapshot(); + + act(() => navType$.next('legacy' as NavType)); + component.update(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 6280d68587355..d24b342e0386b 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -28,9 +28,11 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Component, createRef } from 'react'; import classnames from 'classnames'; -import * as Rx from 'rxjs'; +import React, { createRef, useState } from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import { LoadingIndicator } from '../'; import { ChromeBadge, ChromeBreadcrumb, @@ -41,192 +43,101 @@ import { import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; -import { HeaderBadge } from './header_badge'; import { NavType, OnIsLockedUpdate } from './'; +import { CollapsibleNav } from './collapsible_nav'; +import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; -import { HeaderNavControls } from './header_nav_controls'; -import { createNavLink, createRecentNavLink } from './nav_link'; import { HeaderLogo } from './header_logo'; +import { HeaderNavControls } from './header_nav_controls'; import { NavDrawer } from './nav_drawer'; -import { CollapsibleNav } from './collapsible_nav'; export interface HeaderProps { kibanaVersion: string; application: InternalApplicationStart; - appTitle$: Rx.Observable; - badge$: Rx.Observable; - breadcrumbs$: Rx.Observable; + appTitle$: Observable; + badge$: Observable; + breadcrumbs$: Observable; homeHref: string; - isVisible$: Rx.Observable; + isVisible$: Observable; kibanaDocLink: string; - navLinks$: Rx.Observable; - recentlyAccessed$: Rx.Observable; - forceAppSwitcherNavigation$: Rx.Observable; - helpExtension$: Rx.Observable; - helpSupportUrl$: Rx.Observable; + navLinks$: Observable; + recentlyAccessed$: Observable; + forceAppSwitcherNavigation$: Observable; + helpExtension$: Observable; + helpSupportUrl$: Observable; legacyMode: boolean; - navControlsLeft$: Rx.Observable; - navControlsRight$: Rx.Observable; + navControlsLeft$: Observable; + navControlsRight$: Observable; basePath: HttpStart['basePath']; - isLocked$: Rx.Observable; - navType$: Rx.Observable; + isLocked$: Observable; + navType$: Observable; + loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; } -interface State { - appTitle: string; - isVisible: boolean; - navLinks: ChromeNavLink[]; - recentlyAccessed: ChromeRecentlyAccessedHistoryItem[]; - forceNavigation: boolean; - navControlsLeft: readonly ChromeNavControl[]; - navControlsRight: readonly ChromeNavControl[]; - currentAppId: string | undefined; - isLocked: boolean; - navType: NavType; - isOpen: boolean; +function renderMenuTrigger(toggleOpen: () => void) { + return ( + + + + ); } -export class Header extends Component { - private subscription?: Rx.Subscription; - private navDrawerRef = createRef(); - private toggleCollapsibleNavRef = createRef(); - - constructor(props: HeaderProps) { - super(props); - - let isLocked = false; - props.isLocked$.subscribe((initialIsLocked) => (isLocked = initialIsLocked)); - - this.state = { - appTitle: 'Kibana', - isVisible: true, - navLinks: [], - recentlyAccessed: [], - forceNavigation: false, - navControlsLeft: [], - navControlsRight: [], - currentAppId: '', - isLocked, - navType: 'modern', - isOpen: false, - }; +export function Header({ + kibanaVersion, + kibanaDocLink, + legacyMode, + application, + basePath, + onIsLockedUpdate, + homeHref, + ...observables +}: HeaderProps) { + const isVisible = useObservable(observables.isVisible$, true); + const navType = useObservable(observables.navType$, 'modern'); + const isLocked = useObservable(observables.isLocked$, false); + const [isOpen, setIsOpen] = useState(false); + + if (!isVisible) { + return ; } - public componentDidMount() { - this.subscription = Rx.combineLatest( - this.props.appTitle$, - this.props.isVisible$, - this.props.forceAppSwitcherNavigation$, - this.props.navLinks$, - this.props.recentlyAccessed$, - // Types for combineLatest only handle up to 6 inferred types so we combine these separately. - Rx.combineLatest( - this.props.navControlsLeft$, - this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.isLocked$, - this.props.navType$ - ) - ).subscribe({ - next: ([ - appTitle, - isVisible, - forceNavigation, - navLinks, - recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, isLocked, navType], - ]) => { - this.setState({ - appTitle, - isVisible, - forceNavigation, - navLinks: navLinks.filter((navLink) => !navLink.hidden), - recentlyAccessed, - navControlsLeft, - navControlsRight, - currentAppId, - isLocked, - navType, - }); - }, - }); - } - - public componentWillUnmount() { - if (this.subscription) { - this.subscription.unsubscribe(); + const navDrawerRef = createRef(); + const toggleCollapsibleNavRef = createRef(); + const navId = htmlIdGenerator()(); + const className = classnames( + 'chrHeaderWrapper', // TODO #64541 - delete this + 'hide-for-sharing', + { + 'chrHeaderWrapper--navIsLocked': isLocked, + headerWrapper: navType === 'modern', } - } - - public renderMenuTrigger() { - return ( - this.navDrawerRef.current?.toggleOpen()} - > - - - ); - } - - public render() { - const { appTitle, isVisible, navControlsLeft, navControlsRight } = this.state; - const { - badge$, - breadcrumbs$, - helpExtension$, - helpSupportUrl$, - kibanaDocLink, - kibanaVersion, - } = this.props; - const navLinks = this.state.navLinks.map((link) => - createNavLink( - link, - this.props.legacyMode, - this.state.currentAppId, - this.props.basePath, - this.props.application.navigateToApp - ) - ); - const recentNavLinks = this.state.recentlyAccessed.map((link) => - createRecentNavLink(link, this.state.navLinks, this.props.basePath) - ); + ); - if (!isVisible) { - return null; - } - - const className = classnames( - 'chrHeaderWrapper', // TODO #64541 - delete this - 'hide-for-sharing', - { - 'chrHeaderWrapper--navIsLocked': this.state.isLocked, - headerWrapper: this.state.navType === 'modern', - } - ); - const navId = htmlIdGenerator()(); - return ( + return ( + <> +
    - {this.state.navType === 'modern' ? ( + {navType === 'modern' ? ( { - this.setState({ isOpen: !this.state.isOpen }); - }} - aria-expanded={this.state.isOpen} - aria-pressed={this.state.isOpen} + onClick={() => setIsOpen(!isOpen)} + aria-expanded={isOpen} + aria-pressed={isOpen} aria-controls={navId} - ref={this.toggleCollapsibleNavRef} + ref={toggleCollapsibleNavRef} > @@ -236,71 +147,79 @@ export class Header extends Component { // Delete this block - {this.renderMenuTrigger()} + {renderMenuTrigger(() => navDrawerRef.current?.toggleOpen())} )} - + - + - + - + - {this.state.navType === 'modern' ? ( + {navType === 'modern' ? ( { - this.setState({ isOpen }); - if (this.toggleCollapsibleNavRef.current) { - this.toggleCollapsibleNavRef.current.focus(); + isLocked={isLocked} + navLinks$={observables.navLinks$} + recentlyAccessed$={observables.recentlyAccessed$} + isOpen={isOpen} + homeHref={homeHref} + basePath={basePath} + legacyMode={legacyMode} + navigateToApp={application.navigateToApp} + onIsLockedUpdate={onIsLockedUpdate} + closeNav={() => { + setIsOpen(false); + if (toggleCollapsibleNavRef.current) { + toggleCollapsibleNavRef.current.focus(); } }} - navigateToApp={this.props.application.navigateToApp} /> ) : ( // TODO #64541 // Delete this block )}
    - ); - } + + ); } diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 0398f162f9af9..7fe2c91087090 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -19,26 +19,23 @@ import { mount } from 'enzyme'; import React from 'react'; -import * as Rx from 'rxjs'; - -import { ChromeBreadcrumb } from '../../chrome_service'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { - const breadcrumbs$ = new Rx.Subject(); - const wrapper = mount(); - - breadcrumbs$.next([{ text: 'First' }]); - // Unfortunately, enzyme won't update the wrapper until we call update. - wrapper.update(); + const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); + const wrapper = mount( + + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }]); + act(() => breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }])); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - breadcrumbs$.next([]); + act(() => breadcrumbs$.next([])); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 54cfc7131cb2b..174c46981db53 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -17,88 +17,36 @@ * under the License. */ -import classNames from 'classnames'; -import React, { Component } from 'react'; -import * as Rx from 'rxjs'; - import { EuiHeaderBreadcrumbs } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; import { ChromeBreadcrumb } from '../../chrome_service'; interface Props { - appTitle?: string; - breadcrumbs$: Rx.Observable; + appTitle$: Observable; + breadcrumbs$: Observable; } -interface State { - breadcrumbs: ChromeBreadcrumb[]; -} - -export class HeaderBreadcrumbs extends Component { - private subscription?: Rx.Subscription; - - constructor(props: Props) { - super(props); +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { + const appTitle = useObservable(appTitle$, 'Kibana'); + const breadcrumbs = useObservable(breadcrumbs$, []); + let crumbs = breadcrumbs; - this.state = { breadcrumbs: [] }; + if (breadcrumbs.length === 0 && appTitle) { + crumbs = [{ text: appTitle }]; } - public componentDidMount() { - this.subscribe(); - } - - public componentDidUpdate(prevProps: Props) { - if (prevProps.breadcrumbs$ === this.props.breadcrumbs$) { - return; - } + crumbs = crumbs.map((breadcrumb, i) => ({ + ...breadcrumb, + 'data-test-subj': classNames( + 'breadcrumb', + breadcrumb['data-test-subj'], + i === 0 && 'first', + i === breadcrumbs.length - 1 && 'last' + ), + })); - this.unsubscribe(); - this.subscribe(); - } - - public componentWillUnmount() { - this.unsubscribe(); - } - - public render() { - return ( - - ); - } - - private subscribe() { - this.subscription = this.props.breadcrumbs$.subscribe((breadcrumbs) => { - this.setState({ - breadcrumbs, - }); - }); - } - - private unsubscribe() { - if (this.subscription) { - this.subscription.unsubscribe(); - delete this.subscription; - } - } - - private getBreadcrumbs() { - let breadcrumbs = this.state.breadcrumbs; - - if (breadcrumbs.length === 0 && this.props.appTitle) { - breadcrumbs = [{ text: this.props.appTitle }]; - } - - return breadcrumbs.map((breadcrumb, i) => ({ - ...breadcrumb, - 'data-test-subj': classNames( - 'breadcrumb', - breadcrumb['data-test-subj'], - i === 0 && 'first', - i === breadcrumbs.length - 1 && 'last' - ), - })); - } + return ; } diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 147c7cf5dc4b1..9bec946b6b76e 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -17,11 +17,13 @@ * under the License. */ -import Url from 'url'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiHeaderLogo } from '@elastic/eui'; -import { NavLink } from './nav_link'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import Url from 'url'; +import { ChromeNavLink } from '../..'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -41,7 +43,7 @@ function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { function onClick( event: React.MouseEvent, forceNavigation: boolean, - navLinks: NavLink[], + navLinks: ChromeNavLink[], navigateToApp: (appId: string) => void ) { const anchor = findClosestAnchor((event as any).nativeEvent.target); @@ -50,7 +52,7 @@ function onClick( } const navLink = navLinks.find((item) => item.href === anchor.href); - if (navLink && navLink.isDisabled) { + if (navLink && navLink.disabled) { event.preventDefault(); return; } @@ -85,12 +87,15 @@ function onClick( interface Props { href: string; - navLinks: NavLink[]; - forceNavigation: boolean; + navLinks$: Observable; + forceNavigation$: Observable; navigateToApp: (appId: string) => void; } -export function HeaderLogo({ href, forceNavigation, navLinks, navigateToApp }: Props) { +export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { + const forceNavigation = useObservable(observables.forceNavigation$, false); + const navLinks = useObservable(observables.navLinks$, []); + return ( ; side: 'left' | 'right'; } -export class HeaderNavControls extends Component { - public render() { - const { navControls } = this.props; - - if (!navControls) { - return null; - } +export function HeaderNavControls({ navControls$, side }: Props) { + const navControls = useObservable(navControls$, []); - return navControls.map(this.renderNavControl); + if (!navControls) { + return null; } // It should be performant to use the index as the key since these are unlikely // to change while Kibana is running. - private renderNavControl = (navControl: ChromeNavControl, index: number) => ( - - - + return ( + <> + {navControls.map((navControl: ChromeNavControl, index: number) => ( + + + + ))} + ); } diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index 17df8569f6307..ee4bff6cc0ac4 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -17,24 +17,39 @@ * under the License. */ -import React from 'react'; +import { EuiHorizontalRule, EuiNavDrawer, EuiNavDrawerGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { NavLink, RecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink } from './nav_link'; import { RecentLinks } from './recent_links'; export interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; isLocked?: boolean; + legacyMode: boolean; + navLinks$: Observable; + recentlyAccessed$: Observable; + navigateToApp: CoreStart['application']['navigateToApp']; onIsLockedUpdate?: OnIsLockedUpdate; - navLinks: NavLink[]; - recentNavLinks: RecentNavLink[]; } -function navDrawerRenderer( - { isLocked, onIsLockedUpdate, navLinks, recentNavLinks }: Props, +function NavDrawerRenderer( + { isLocked, onIsLockedUpdate, basePath, legacyMode, navigateToApp, ...observables }: Props, ref: React.Ref ) { + const appId = useObservable(observables.appId$, ''); + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentNavLinks = useObservable(observables.recentlyAccessed$, []).map((link) => + createRecentNavLink(link, navLinks, basePath) + ); + return ( + createEuiListItem({ + link, + legacyMode, + appId, + basePath, + navigateToApp, + dataTestSubj: 'navDrawerAppsMenuLink', + }) + )} aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', { defaultMessage: 'Primary navigation links', })} @@ -58,4 +82,4 @@ function navDrawerRenderer( ); } -export const NavDrawer = React.forwardRef(navDrawerRenderer); +export const NavDrawer = React.forwardRef(NavDrawerRenderer); diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c979bb8271e1b..c09b15fac9bdb 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiImage } from '@elastic/eui'; -import { AppCategory } from 'src/core/types'; -import { ChromeNavLink, CoreStart, ChromeRecentlyAccessedHistoryItem } from '../../../'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; import { HttpStart } from '../../../http'; +import { relativeToAbsolute } from '../../nav_links/to_nav_link'; function isModifiedEvent(event: React.MouseEvent) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); @@ -32,62 +32,37 @@ function LinkIcon({ url }: { url: string }) { return ; } -export interface NavLink { - key: string; - label: string; - href: string; - isActive: boolean; - onClick(event: React.MouseEvent): void; - category?: AppCategory; - isDisabled?: boolean; - iconType?: string; - icon?: JSX.Element; - order?: number; - 'data-test-subj': string; +interface Props { + link: ChromeNavLink; + legacyMode: boolean; + appId: string | undefined; + basePath?: HttpStart['basePath']; + dataTestSubj: string; + onClick?: Function; + navigateToApp: CoreStart['application']['navigateToApp']; } -/** - * Create a link that's actually ready to be passed into EUI - * - * @param navLink - * @param legacyMode - * @param currentAppId - * @param basePath - * @param navigateToApp - */ -export function createNavLink( - navLink: ChromeNavLink, - legacyMode: boolean, - currentAppId: string | undefined, - basePath: HttpStart['basePath'], - navigateToApp: CoreStart['application']['navigateToApp'] -): NavLink { - const { - legacy, - url, - active, - baseUrl, - id, - title, - disabled, - euiIconType, - icon, - category, - order, - tooltip, - } = navLink; - let href = navLink.url ?? navLink.baseUrl; - - if (legacy) { - href = url && !active ? url : baseUrl; - } +// TODO #64541 +// Set return type to EuiListGroupItemProps +// Currently it's a subset of EuiListGroupItemProps+FlyoutMenuItem for CollapsibleNav and NavDrawer +// But FlyoutMenuItem isn't exported from EUI +export function createEuiListItem({ + link, + legacyMode, + appId, + basePath, + onClick = () => {}, + navigateToApp, + dataTestSubj, +}: Props) { + const { legacy, active, id, title, disabled, euiIconType, icon, tooltip, href } = link; return { - category, - key: id, label: tooltip ?? title, - href, // Use href and onClick to support "open in new tab" and SPA navigation in the same link - onClick(event) { + href, + /* Use href and onClick to support "open in new tab" and SPA navigation in the same link */ + onClick(event: React.MouseEvent) { + onClick(); if ( !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps @@ -96,57 +71,31 @@ export function createNavLink( !isModifiedEvent(event) // ignore clicks with modifier keys ) { event.preventDefault(); - navigateToApp(navLink.id); + navigateToApp(id); } }, // Legacy apps use `active` property, NP apps should match the current app - isActive: active || currentAppId === id, + isActive: active || appId === id, isDisabled: disabled, - iconType: euiIconType, - icon: !euiIconType && icon ? : undefined, - order, - 'data-test-subj': 'navDrawerAppsMenuLink', + 'data-test-subj': dataTestSubj, + ...(basePath && { + iconType: euiIconType, + icon: !euiIconType && icon ? : undefined, + }), }; } -// Providing a buffer between the limit and the cut off index -// protects from truncating just the last couple (6) characters -const TRUNCATE_LIMIT: number = 64; -const TRUNCATE_AT: number = 58; - -function truncateRecentItemLabel(label: string): string { - if (label.length > TRUNCATE_LIMIT) { - label = `${label.substring(0, TRUNCATE_AT)}…`; - } - - return label; -} - -/** - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -function relativeToAbsolute(url: string) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - export interface RecentNavLink { href: string; label: string; title: string; 'aria-label': string; iconType?: string; - onClick?(event: React.MouseEvent): void; } /** * Add saved object type info to recently links + * TODO #64541 - set return type to EuiListGroupItemProps * * Recent nav links are similar to normal nav links but are missing some Kibana Platform magic and * because of legacy reasons have slightly different properties. @@ -176,7 +125,7 @@ export function createRecentNavLink( return { href, - label: truncateRecentItemLabel(label), + label, title: titleAndAriaLabel, 'aria-label': titleAndAriaLabel, iconType: navLink?.euiIconType, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4ccded9b9afec..90c5dbb5f6558 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -286,6 +286,7 @@ export interface ChromeNavLink { readonly disableSubUrlTracking?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; + readonly href?: string; readonly icon?: string; readonly id: string; // @internal @@ -314,7 +315,7 @@ export interface ChromeNavLinks { } // @public (undocumented) -export type ChromeNavLinkUpdateableFields = Partial>; +export type ChromeNavLinkUpdateableFields = Partial>; // @public export interface ChromeRecentlyAccessed { diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 2567ca790e04f..98f8d874c7088 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -151,7 +151,7 @@ export type LegacyAppSpec = Partial & { * @internal * @deprecated */ -export type LegacyNavLink = Omit & { +export type LegacyNavLink = Omit & { order: number; }; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 32abfd2694f16..142ec9c8c877e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1366,7 +1366,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -1578,8 +1578,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts From 8f6bef10126fa878411ad3e05626aff75437f1ee Mon Sep 17 00:00:00 2001 From: Eric Beahan Date: Fri, 29 May 2020 09:59:58 -0500 Subject: [PATCH 17/38] Update table of contents to reflect current content (#66835) --- CONTRIBUTING.md | 56 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1053cc2f65396..4bf659345d387 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,26 +13,44 @@ A high level overview of our contributing guidelines. - ["My issue isn't getting enough attention"](#my-issue-isnt-getting-enough-attention) - ["I want to help!"](#i-want-to-help) - [How We Use Git and GitHub](#how-we-use-git-and-github) + - [Forking](#forking) - [Branching](#branching) - [Commits and Merging](#commits-and-merging) + - [Rebasing and fixing merge conflicts](#rebasing-and-fixing-merge-conflicts) - [What Goes Into a Pull Request](#what-goes-into-a-pull-request) - [Contributing Code](#contributing-code) - [Setting Up Your Development Environment](#setting-up-your-development-environment) + - [Increase node.js heap size](#increase-nodejs-heap-size) + - [Running Elasticsearch Locally](#running-elasticsearch-locally) + - [Nightly snapshot (recommended)](#nightly-snapshot-recommended) + - [Keeping data between snapshots](#keeping-data-between-snapshots) + - [Source](#source) + - [Archive](#archive) + - [Sample Data](#sample-data) + - [Running Elasticsearch Remotely](#running-elasticsearch-remotely) + - [Running remote clusters](#running-remote-clusters) + - [Running Kibana](#running-kibana) + - [Running Kibana in Open-Source mode](#running-kibana-in-open-source-mode) + - [Unsupported URL Type](#unsupported-url-type) - [Customizing `config/kibana.dev.yml`](#customizing-configkibanadevyml) + - [Potential Optimization Pitfalls](#potential-optimization-pitfalls) - [Setting Up SSL](#setting-up-ssl) - [Linting](#linting) + - [Setup Guide for VS Code Users](#setup-guide-for-vs-code-users) - [Internationalization](#internationalization) - [Localization](#localization) + - [Styling with SASS](#styling-with-sass) - [Testing and Building](#testing-and-building) - [Debugging server code](#debugging-server-code) - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) - - [Debugging Unit Tests](#debugging-unit-tests) - - [Unit Testing Plugins](#unit-testing-plugins) - - [Automated Accessibility Testing](#automated-accessibility-testing) - - [Cross-browser compatibility](#cross-browser-compatibility) - - [Testing compatibility locally](#testing-compatibility-locally) - - [Running Browser Automation Tests](#running-browser-automation-tests) - - [Browser Automation Notes](#browser-automation-notes) + - [Unit testing frameworks](#unit-testing-frameworks) + - [Running specific Kibana tests](#running-specific-kibana-tests) + - [Debugging Unit Tests](#debugging-unit-tests) + - [Unit Testing Plugins](#unit-testing-plugins) + - [Automated Accessibility Testing](#automated-accessibility-testing) + - [Cross-browser compatibility](#cross-browser-compatibility) + - [Testing compatibility locally](#testing-compatibility-locally) + - [Running Browser Automation Tests](#running-browser-automation-tests) - [Building OS packages](#building-os-packages) - [Writing documentation](#writing-documentation) - [Release Notes Process](#release-notes-process) @@ -414,7 +432,7 @@ extract them to a `JSON` file or integrate translations back to Kibana. To know We cannot support accepting contributions to the translations from any source other than the translators we have engaged to do the work. We are still to develop a proper process to accept any contributed translations. We certainly appreciate that people care enough about the localization effort to want to help improve the quality. We aim to build out a more comprehensive localization process for the future and will notify you once contributions can be supported, but for the time being, we are not able to incorporate suggestions. -### Syling with SASS +### Styling with SASS When writing a new component, create a sibling SASS file of the same name and import directly into the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). @@ -467,10 +485,10 @@ macOS users on a machine with a discrete graphics card may see significant speed - Uncheck the "Prefer integrated to discrete GPU" option - Restart iTerm -### Debugging Server Code +#### Debugging Server Code `yarn debug` will start the server with Node's inspect flag. Kibana's development mode will start three processes on ports `9229`, `9230`, and `9231`. Chrome's developer tools need to be configured to connect to all three connections. Add `localhost:` for each Kibana process in Chrome's developer tools connection tab. -### Instrumenting with Elastic APM +#### Instrumenting with Elastic APM Kibana ships with the [Elastic APM Node.js Agent](https://github.com/elastic/apm-agent-nodejs) built-in for debugging purposes. Its default configuration is meant to be used by core Kibana developers only, but it can easily be re-configured to your needs. @@ -501,13 +519,13 @@ ELASTIC_APM_ACTIVE=true yarn start Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics. The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana. -### Unit testing frameworks +#### Unit testing frameworks Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still exist in Mocha but all new unit tests should be written in Jest. Mocha tests are contained in `__tests__` directories. Whereas Jest tests are stored in the same directory as source code files with the `.test.js` suffix. -### Running specific Kibana tests +#### Running specific Kibana tests The following table outlines possible test file locations and how to invoke them: @@ -540,7 +558,7 @@ Test runner arguments: yarn test:ftr:runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets' ``` -### Debugging Unit Tests +#### Debugging Unit Tests The standard `yarn test` task runs several sub tasks and can take several minutes to complete, making debugging failures pretty painful. In order to ease the pain specialized tasks provide alternate methods for running the tests. @@ -567,7 +585,7 @@ In the screenshot below, you'll notice the URL is `localhost:9876/debug.html`. Y ![Browser test debugging](http://i.imgur.com/DwHxgfq.png) -### Unit Testing Plugins +#### Unit Testing Plugins This should work super if you're using the [Kibana plugin generator](https://github.com/elastic/kibana/tree/master/packages/kbn-plugin-generator). If you're not using the generator, well, you're on your own. We suggest you look at how the generator works. @@ -578,7 +596,7 @@ yarn test:mocha yarn test:karma:debug # remove the debug flag to run them once and close ``` -### Automated Accessibility Testing +#### Automated Accessibility Testing To run the tests locally: @@ -595,11 +613,11 @@ can be run locally using their browser plugins: - [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US) - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) -### Cross-browser Compatibility +#### Cross-browser Compatibility -#### Testing Compatibility Locally +##### Testing Compatibility Locally -##### Testing IE on OS X +###### Testing IE on OS X * [Download VMWare Fusion](http://www.vmware.com/products/fusion/fusion-evaluation.html). * [Download IE virtual machines](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/#downloads) for VMWare. @@ -610,7 +628,7 @@ can be run locally using their browser plugins: * Now you can run your VM, open the browser, and navigate to `http://computer.local:5601` to test Kibana. * Alternatively you can use browserstack -#### Running Browser Automation Tests +##### Running Browser Automation Tests [Read about the `FunctionalTestRunner`](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html) to learn more about how you can run and develop functional tests for Kibana core and plugins. From 761465bc77d66a9449be676873433bf148ccb493 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 29 May 2020 18:22:43 +0200 Subject: [PATCH 18/38] clean up kibana-app ownership (#67780) --- .github/CODEOWNERS | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 42bf7662ff2e1..c3da7c7f00e96 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,10 +6,7 @@ /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app -/src/legacy/server/url_shortening/ @elastic/kibana-app -/src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app From 1d5933b9a6de41a455f3ab949199f6bb7ae20b2e Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 29 May 2020 09:40:46 -0700 Subject: [PATCH 19/38] Changed AlertsClient to use ActionsClient instead of direct interaction with the `action` saved objects (#67562) --- .../actions/server/actions_client.mock.ts | 1 + .../actions/server/actions_client.test.ts | 68 +++++++++++++ .../plugins/actions/server/actions_client.ts | 40 +++++++- .../alerting/server/alerts_client.test.ts | 97 +++++++------------ .../plugins/alerting/server/alerts_client.ts | 86 ++++++---------- .../server/alerts_client_factory.test.ts | 15 ++- .../alerting/server/alerts_client_factory.ts | 14 +-- x-pack/plugins/alerting/server/plugin.ts | 2 +- 8 files changed, 192 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 64b43e1ab6bbc..a2b64e49f76e3 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -16,6 +16,7 @@ const createActionsClientMock = () => { delete: jest.fn(), update: jest.fn(), getAll: jest.fn(), + getBulk: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 0132cc8bdb01a..bf55a1c18d169 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -423,6 +423,74 @@ describe('getAll()', () => { }); }); +describe('getBulk()', () => { + test('calls getBulk savedObjectsClient with parameters', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + references: [], + }, + ], + }); + scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ + aggregations: { + '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, + }, + }); + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + const result = await actionsClient.getBulk(['1', 'testPreconfigured']); + expect(result).toEqual([ + { + actionTypeId: '.slack', + config: { + foo: 'bar', + }, + id: 'testPreconfigured', + isPreconfigured: true, + name: 'test', + secrets: {}, + }, + { + actionTypeId: 'test', + config: { + foo: 'bar', + }, + id: '1', + isPreconfigured: false, + name: 'test', + }, + ]); + }); +}); + describe('delete()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index c9052cf53d948..48703f01f5509 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -3,7 +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 Boom from 'boom'; import { IScopedClusterClient, SavedObjectsClientContract, @@ -193,6 +193,44 @@ export class ActionsClient { ); } + /** + * Get bulk actions with preconfigured list + */ + public async getBulk(ids: string[]): Promise { + const actionResults = new Array(); + for (const actionId of ids) { + const action = this.preconfiguredActions.find( + (preconfiguredAction) => preconfiguredAction.id === actionId + ); + if (action !== undefined) { + actionResults.push(action); + } + } + + // Fetch action objects in bulk + // Excluding preconfigured actions to avoid an not found error, which is already added + const actionSavedObjectsIds = [ + ...new Set( + ids.filter( + (actionId) => !actionResults.find((actionResult) => actionResult.id === actionId) + ) + ), + ]; + + const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' })); + const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + + for (const action of bulkGetResult.saved_objects) { + if (action.error) { + throw Boom.badRequest( + `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` + ); + } + actionResults.push(actionFromSavedObject(action)); + } + return actionResults; + } + /** * Delete action */ diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index fa86c27651136..12106100602e7 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -13,6 +13,7 @@ import { TaskStatus } from '../../../plugins/task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { actionsClientMock } from '../../actions/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -30,7 +31,7 @@ const alertsClientParams = { invalidateAPIKey: jest.fn(), logger: loggingServiceMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, - preconfiguredActions: [], + getActionsClient: jest.fn(), }; beforeEach(() => { @@ -42,6 +43,34 @@ beforeEach(() => { }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + isPreconfigured: false, + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + { + id: '2', + isPreconfigured: false, + actionTypeId: 'test2', + name: 'test2', + config: { + foo: 'bar', + }, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); @@ -97,18 +126,6 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - ], - }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -297,26 +314,6 @@ describe('create()', () => { }, ], }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -435,16 +432,6 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ - { - id: '1', - type: 'action', - }, - { - id: '2', - type: 'action', - }, - ]); }); test('creates a disabled alert', async () => { @@ -549,7 +536,9 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockRejectedValueOnce(new Error('Test Error')); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); @@ -1903,26 +1892,6 @@ describe('update()', () => { }); test('updates given parameters', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); savedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index e43939e2f44c3..382e9d1a616ad 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -13,7 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; -import { PreConfiguredAction } from '../../actions/server'; +import { ActionsClient } from '../../actions/server'; import { Alert, PartialAlert, @@ -24,7 +24,6 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, - RawAlertAction, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -56,7 +55,7 @@ interface ConstructorOptions { getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; - preconfiguredActions: PreConfiguredAction[]; + getActionsClient: () => Promise; } export interface FindOptions { @@ -127,7 +126,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - private preconfiguredActions: PreConfiguredAction[]; + private readonly getActionsClient: () => Promise; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; constructor({ @@ -141,7 +140,7 @@ export class AlertsClient { createAPIKey, invalidateAPIKey, encryptedSavedObjectsClient, - preconfiguredActions, + getActionsClient, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -153,7 +152,7 @@ export class AlertsClient { this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; - this.preconfiguredActions = preconfiguredActions; + this.getActionsClient = getActionsClient; } public async create({ data, options }: CreateOptions): Promise { @@ -600,7 +599,7 @@ export class AlertsClient { actions: RawAlert['actions'], references: SavedObjectReference[] ) { - return actions.map((action, i) => { + return actions.map((action) => { const reference = references.find((ref) => ref.name === action.actionRef); if (!reference) { throw new Error(`Reference ${action.actionRef} not found`); @@ -666,58 +665,31 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionMap = new Map(); - // map preconfigured actions - for (const alertAction of alertActions) { - const action = this.preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === alertAction.id - ); - if (action !== undefined) { - actionMap.set(action.id, action); - } - } - // Fetch action objects in bulk - // Excluding preconfigured actions to avoid an not found error, which is already mapped - const actionIds = [ - ...new Set( - alertActions - .filter((alertAction) => !actionMap.has(alertAction.id)) - .map((alertAction) => alertAction.id) - ), - ]; - if (actionIds.length > 0) { - const bulkGetOpts = actionIds.map((id) => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); - - for (const action of bulkGetResult.saved_objects) { - if (action.error) { - throw Boom.badRequest( - `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` - ); - } - actionMap.set(action.id, action); - } - } - // Extract references and set actionTypeId + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionResults = await actionsClient.getBulk(actionIds); const references: SavedObjectReference[] = []; const actions = alertActions.map(({ id, ...alertAction }, i) => { - const actionRef = `action_${i}`; - references.push({ - id, - name: actionRef, - type: 'action', - }); - const actionMapValue = actionMap.get(id); - // if action is a save object, than actionTypeId should be under attributes property - // if action is a preconfigured, than actionTypeId is the action property - const actionTypeId = actionIds.find((actionId) => actionId === id) - ? (actionMapValue as SavedObject>).attributes.actionTypeId - : (actionMapValue as RawAlertAction).actionTypeId; - return { - ...alertAction, - actionRef, - actionTypeId, - }; + const actionResultValue = actionResults.find((action) => action.id === id); + if (actionResultValue) { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + return { + ...alertAction, + actionRef, + actionTypeId: actionResultValue.actionTypeId, + }; + } else { + return { + ...alertAction, + actionRef: '', + actionTypeId: '', + }; + } }); return { actions, diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index cc792d11c890d..d1a7c60bb9a68 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -13,6 +13,7 @@ import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { AuthenticatedUser } from '../../../plugins/security/public'; import { securityMock } from '../../../plugins/security/server/mocks'; +import { actionsMock } from '../../actions/server/mocks'; jest.mock('./alerts_client'); @@ -25,7 +26,7 @@ const alertsClientFactoryParams: jest.Mocked = { getSpaceId: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), - preconfiguredActions: [], + actions: actionsMock.createStart(), }; const fakeRequest = ({ headers: {}, @@ -65,7 +66,7 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, - preconfiguredActions: [], + getActionsClient: expect.any(Function), }); }); @@ -95,6 +96,16 @@ test('getUserName() returns a name when security is enabled', async () => { expect(userNameResult).toEqual('bob'); }); +test('getActionsClient() returns ActionsClient', async () => { + const factory = new AlertsClientFactory(); + factory.initialize(alertsClientFactoryParams); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; + + const actionsClient = await constructorCall.getActionsClient(); + expect(actionsClient).not.toBe(null); +}); + test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 913b4e2e81fe1..2924736330abd 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PreConfiguredAction } from '../../actions/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; @@ -20,7 +20,7 @@ export interface AlertsClientFactoryOpts { getSpaceId: (request: KibanaRequest) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - preconfiguredActions: PreConfiguredAction[]; + actions: ActionsPluginStartContract; } export class AlertsClientFactory { @@ -32,7 +32,7 @@ export class AlertsClientFactory { private getSpaceId!: (request: KibanaRequest) => string | undefined; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; - private preconfiguredActions!: PreConfiguredAction[]; + private actions!: ActionsPluginStartContract; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -46,14 +46,14 @@ export class AlertsClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; - this.preconfiguredActions = options.preconfiguredActions; + this.actions = options.actions; } public create( request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract ): AlertsClient { - const { securityPluginSetup } = this; + const { securityPluginSetup, actions } = this; const spaceId = this.getSpaceId(request); return new AlertsClient({ spaceId, @@ -104,7 +104,9 @@ export class AlertsClientFactory { result: invalidateAPIKeyResult, }; }, - preconfiguredActions: this.preconfiguredActions, + async getActionsClient() { + return actions.getActionsClientWithRequest(request); + }, }); } } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 33fb8d9e0d212..e789e655774a0 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -216,7 +216,7 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return spaces?.getSpaceId(request); }, - preconfiguredActions: plugins.actions.preconfiguredActions, + actions: plugins.actions, }); taskRunnerFactory.initialize({ From 6b7b0cbc44769046a969e7789203f8f148747e33 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Fri, 29 May 2020 13:33:46 -0400 Subject: [PATCH 20/38] [Endpoint]EMT: temporarily skip test till package update. (#67778) [Endpoint]EMT: temporarily skip test till package update. --- x-pack/test/api_integration/apis/endpoint/alerts/index.ts | 2 +- .../test/api_integration/apis/endpoint/alerts/index_pattern.ts | 2 +- x-pack/test/api_integration/apis/endpoint/metadata.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts index 155513aefc609..ecdee09ce7edf 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts @@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) { let nullableEventId = ''; - describe('Endpoint alert API', () => { + describe.skip('Endpoint alert API', () => { describe('when data is in elasticsearch', () => { before(async () => { await esArchiver.load('endpoint/alerts/api_feature'); diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts index e87b063453054..df1cbcfe28e7b 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Endpoint index pattern API', () => { + describe.skip('Endpoint index pattern API', () => { it('should retrieve the index pattern for events', async () => { const { body } = await supertest.get('/api/endpoint/index_pattern/events').expect(200); expect(body.indexPattern).to.eql('events-endpoint-*'); diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 5c4bb52b8d9e2..c01919f60a922 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -14,7 +14,7 @@ const numberOfHostsInFixture = 3; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + describe.skip('test metadata api', () => { describe('POST /api/endpoint/metadata when index is empty', () => { it('metadata api should return empty result when index is empty', async () => { await esArchiver.unload('endpoint/metadata/api_feature'); From 6288096f622489877d20ed3de381e8df37a574f0 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 29 May 2020 10:34:58 -0700 Subject: [PATCH 21/38] [kbn/optimizer] use execa to fork workers (#67730) Co-authored-by: spalger --- packages/kbn-optimizer/package.json | 1 + .../src/optimizer/observe_worker.ts | 14 ++++---- packages/kbn-pm/dist/index.js | 34 +++++++++---------- yarn.lock | 8 ++--- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b7c9a63897bf9..7bd7a236a43aa 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -28,6 +28,7 @@ "cpy": "^8.0.0", "css-loader": "^3.4.2", "del": "^5.1.0", + "execa": "^4.0.2", "file-loader": "^4.2.0", "istanbul-instrumenter-loader": "^3.0.1", "jest-diff": "^25.1.0", diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index f5c944cefb76f..c929cf62d1bb0 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -17,10 +17,10 @@ * under the License. */ -import { fork, ChildProcess } from 'child_process'; import { Readable } from 'stream'; import { inspect } from 'util'; +import execa from 'execa'; import * as Rx from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; @@ -42,7 +42,7 @@ export interface WorkerStarted { export type WorkerStatus = WorkerStdio | WorkerStarted; interface ProcResource extends Rx.Unsubscribable { - proc: ChildProcess; + proc: execa.ExecaChildProcess; } const isNumeric = (input: any) => String(input).match(/^[0-9]+$/); @@ -70,20 +70,22 @@ function usingWorkerProc( config: OptimizerConfig, workerConfig: WorkerConfig, bundles: Bundle[], - fn: (proc: ChildProcess) => Rx.Observable + fn: (proc: execa.ExecaChildProcess) => Rx.Observable ) { return Rx.using( (): ProcResource => { const args = [JSON.stringify(workerConfig), JSON.stringify(bundles.map((b) => b.toSpec()))]; - const proc = fork(require.resolve('../worker/run_worker'), args, { - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - execArgv: [ + const proc = execa.node(require.resolve('../worker/run_worker'), args, { + nodeOptions: [ ...(inspectFlag && config.inspectWorkers ? [`${inspectFlag}=${inspectPortCounter++}`] : []), ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), ], + buffer: false, + stderr: 'pipe', + stdout: 'pipe', }); return { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index baaac3d8d4a86..21fff4d85ece6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -34812,10 +34812,11 @@ const makeError = ({ const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); const execaMessage = `Command ${prefix}: ${command}`; - const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage; + const isError = Object.prototype.toString.call(error) === '[object Error]'; + const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); - if (error instanceof Error) { + if (isError) { error.originalMessage = error.message; error.message = message; } else { @@ -36263,25 +36264,24 @@ module.exports = function (/*streams...*/) { "use strict"; -const mergePromiseProperty = (spawned, promise, property) => { - // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' ? - (...args) => promise()[property](...args) : - promise[property].bind(promise); - Object.defineProperty(spawned, property, { - value, - writable: true, - enumerable: false, - configurable: true - }); -}; +const nativePromisePrototype = (async () => {})().constructor.prototype; +const descriptors = ['then', 'catch', 'finally'].map(property => [ + property, + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property) +]); // The return value is a mixin of `childProcess` and `Promise` const mergePromise = (spawned, promise) => { - mergePromiseProperty(spawned, promise, 'then'); - mergePromiseProperty(spawned, promise, 'catch'); - mergePromiseProperty(spawned, promise, 'finally'); + for (const [property, descriptor] of descriptors) { + // Starting the main `promise` is deferred to avoid consuming streams + const value = typeof promise === 'function' ? + (...args) => Reflect.apply(descriptor.value, promise(), args) : + descriptor.value.bind(promise); + + Reflect.defineProperty(spawned, property, {...descriptor, value}); + } + return spawned; }; diff --git a/yarn.lock b/yarn.lock index e5463de13ea00..10f338b414456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13092,10 +13092,10 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" - integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA== +execa@^4.0.0, execa@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" + integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" From fbb5f3169892803f42ace5fa0ef02c9362126c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 29 May 2020 20:00:33 +0200 Subject: [PATCH 22/38] =?UTF-8?q?[APM]=20Don=E2=80=99t=20run=20eslint=20on?= =?UTF-8?q?=20cypress=20snapshots=20(#67451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Don’t run eslint on cypress snapshots * ignore cypress videos * Fix interactive command * Fix gitignore * Use echo everywhere Co-authored-by: Elastic Machine --- .eslintignore | 3 +- .gitignore | 8 ++-- x-pack/plugins/apm/e2e/.gitignore | 3 +- x-pack/plugins/apm/e2e/run-e2e.sh | 72 +++++++++++++++++++++---------- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/.eslintignore b/.eslintignore index c3d7930732fa2..fbdd70703f3c4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,7 +26,8 @@ target /src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** -/x-pack/plugins/apm/e2e/cypress/**/snapshots.js +/x-pack/plugins/apm/e2e/**/snapshots.js +/x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/canvas/shareable_runtime/build diff --git a/.gitignore b/.gitignore index f843609d32f7e..b3911d0f8d0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,8 @@ package-lock.json *.sublime-* npm-debug.log* .tern-project -x-pack/plugins/apm/tsconfig.json -apm.tsconfig.json -/x-pack/legacy/plugins/apm/e2e/snapshots.js -/x-pack/plugins/apm/e2e/snapshots.js .nyc_output + +# apm plugin +/x-pack/plugins/apm/tsconfig.json +apm.tsconfig.json diff --git a/x-pack/plugins/apm/e2e/.gitignore b/x-pack/plugins/apm/e2e/.gitignore index 9eb738ede51e3..5042f0bca0300 100644 --- a/x-pack/plugins/apm/e2e/.gitignore +++ b/x-pack/plugins/apm/e2e/.gitignore @@ -1,4 +1,5 @@ cypress/screenshots/* -cypress/videos/* cypress/test-results +cypress/videos/* +/snapshots.js tmp diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 157c42cc7e4ee..ae764d171c45c 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -26,20 +26,23 @@ cd ${E2E_DIR} # # Ask user to start Kibana ################################################## -echo "\n${bold}To start Kibana please run the following command:${normal} +echo "" # newline +echo "${bold}To start Kibana please run the following command:${normal} node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/plugins/apm/e2e/ci/kibana.e2e.yml" # # Create tmp folder ################################################## -echo "\n${bold}Temporary folder${normal}" -echo "Temporary files will be stored in: ${TMP_DIR}" +echo "" # newline +echo "${bold}Temporary folder${normal}" +echo "Temporary files will be stored in: ${E2E_DIR}${TMP_DIR}" mkdir -p ${TMP_DIR} # # apm-integration-testing ################################################## -printf "\n${bold}apm-integration-testing (logs: ${TMP_DIR}/apm-it.log)\n${normal}" +echo "" # newline +echo "${bold}apm-integration-testing (logs: ${E2E_DIR}${TMP_DIR}/apm-it.log)${normal}" # pull if folder already exists if [ -d ${APM_IT_DIR} ]; then @@ -54,7 +57,7 @@ fi # Stop if clone/pull failed if [ $? -ne 0 ]; then - printf "\n⚠️ Initializing apm-integration-testing failed. \n" + echo "⚠️ Initializing apm-integration-testing failed." exit 1 fi @@ -71,23 +74,34 @@ ${APM_IT_DIR}/scripts/compose.py start master \ # Stop if apm-integration-testing failed to start correctly if [ $? -ne 0 ]; then - printf "⚠️ apm-integration-testing could not be started.\n" - printf "Please see the logs in ${TMP_DIR}/apm-it.log\n\n" - printf "As a last resort, reset docker with:\n\n cd ${APM_IT_DIR} && scripts/compose.py stop && docker system prune --all --force --volumes\n" + echo "⚠️ apm-integration-testing could not be started" + echo "" # newline + echo "As a last resort, reset docker with:" + echo "" # newline + echo "cd ${E2E_DIR}${APM_IT_DIR} && scripts/compose.py stop && docker system prune --all --force --volumes" + echo "" # newline + + # output logs for excited docker containers + cd ${APM_IT_DIR} && docker-compose ps --filter "status=exited" -q | xargs -L1 docker logs --tail=10 && cd - + + echo "" # newline + echo "Find the full logs in ${E2E_DIR}${TMP_DIR}/apm-it.log" exit 1 fi # # Cypress ################################################## -echo "\n${bold}Cypress (logs: ${TMP_DIR}/e2e-yarn.log)${normal}" +echo "" # newline +echo "${bold}Cypress (logs: ${E2E_DIR}${TMP_DIR}/e2e-yarn.log)${normal}" echo "Installing cypress dependencies " yarn &> ${TMP_DIR}/e2e-yarn.log # # Static mock data ################################################## -printf "\n${bold}Static mock data (logs: ${TMP_DIR}/ingest-data.log)\n${normal}" +echo "" # newline +echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${normal}" # Download static data if not already done if [ ! -e "${TMP_DIR}/events.json" ]; then @@ -102,16 +116,32 @@ curl --silent --user admin:changeme -XDELETE "localhost:${ELASTICSEARCH_PORT}/ap # Ingest data into APM Server node ingest-data/replay.js --server-url http://localhost:$APM_SERVER_PORT --events ${TMP_DIR}/events.json 2>> ${TMP_DIR}/ingest-data.log -# Stop if not all events were ingested correctly +# Abort if not all events were ingested correctly if [ $? -ne 0 ]; then - printf "\n⚠️ Not all events were ingested correctly. This might affect test tests. \n" + echo "⚠️ Not all events were ingested correctly. This might affect test tests." + echo "Aborting. Please try again." + echo "" # newline + echo "Full logs in ${E2E_DIR}${TMP_DIR}/ingest-data.log:" + + # output logs for excited docker containers + cd ${APM_IT_DIR} && docker-compose ps --filter "status=exited" -q | xargs -L1 docker logs --tail=3 && cd - + + # stop docker containers + cd ${APM_IT_DIR} && ./scripts/compose.py stop > /dev/null && cd - exit 1 fi +# create empty snapshot file if it doesn't exist +SNAPSHOTS_FILE=cypress/integration/snapshots.js +if [ ! -f ${SNAPSHOTS_FILE} ]; then + echo "{}" > ${SNAPSHOTS_FILE} +fi + # # Wait for Kibana to start ################################################## -echo "\n${bold}Waiting for Kibana to start...${normal}" +echo "" # newline +echo "${bold}Waiting for Kibana to start...${normal}" echo "Note: you need to start Kibana manually. Find the instructions at the top." yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/status > /dev/null @@ -119,12 +149,13 @@ yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/ ## See: https://github.com/elastic/kibana/issues/66326 if [ -e kibana.log ] ; then grep -m 1 "http server running" <(tail -f -n +1 kibana.log) - echo "\n✅ Kibana server running...\n" + echo "✅ Kibana server running..." grep -m 1 "bundles compiled successfully" <(tail -f -n +1 kibana.log) - echo "\n✅ Kibana bundles have been compiled...\n" + echo "✅ Kibana bundles have been compiled..." fi -echo "\n✅ Setup completed successfully. Running tests...\n" + +echo "✅ Setup completed successfully. Running tests..." # # run cypress tests @@ -134,9 +165,6 @@ yarn cypress run --config pageLoadTimeout=100000,watchForFileChanges=true # # Run interactively ################################################## -echo " - -${bold}If you want to run the test interactively, run:${normal} - -yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true -" +echo "${bold}If you want to run the test interactively, run:${normal}" +echo "" # newline +echo "cd ${E2E_DIR} && yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true" From 81d55f8822a29b6c1310bdb4f52a66f1fdcbb4ba Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 29 May 2020 14:58:40 -0400 Subject: [PATCH 23/38] [CI] Bump chromedriver and use DETECT_CHROMEDRIVER_VERSION (#67642) --- package.json | 2 +- src/dev/ci_setup/setup_env.sh | 10 +++++++++- yarn.lock | 8 ++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2dca52121d056..cc1f7eb6c1dd3 100644 --- a/package.json +++ b/package.json @@ -408,7 +408,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^81.0.0", + "chromedriver": "^83.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index d9d0528748dc0..343ff47199375 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -128,9 +128,17 @@ export GECKODRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfuncti export CHROMEDRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" - export CHECKS_REPORTER_ACTIVE=false +# This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed +if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then + echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" + export DETECT_CHROMEDRIVER_VERSION=true + export CHROMEDRIVER_FORCE_DOWNLOAD=true +else + echo "Chrome not detected, installing default chromedriver binary for the package version" +fi + ### only run on pr jobs for elastic/kibana, checks-reporter doesn't work for other repos if [[ "$ghprbPullId" && "$ghprbGhRepository" == 'elastic/kibana' ]] ; then export CHECKS_REPORTER_ACTIVE=true diff --git a/yarn.lock b/yarn.lock index 10f338b414456..5d47056857bbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8945,10 +8945,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^81.0.0: - version "81.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-81.0.0.tgz#690ba333aedf2b4c4933b6590c3242d3e5f28f3c" - integrity sha512-BA++IQ7O1FzHmNpzMlOfLiSBvPZ946uuhtJjZHEIr/Gb+Ha9jiuGbHiT45l6O3XGbQ8BAwvbmdisjl4rTxro4A== +chromedriver@^83.0.0: + version "83.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-83.0.0.tgz#75d7d838e58014658c3990089464166fef951926" + integrity sha512-AePp9ykma+z4aKPRqlbzvVlc22VsQ6+rgF+0aL3B5onHOncK18dWSkLrSSJMczP/mXILN9ohGsvpuTwoRSj6OQ== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" From a63adabd3816e8a10cf35cf94c576a2f709bb30a Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 29 May 2020 12:15:05 -0700 Subject: [PATCH 24/38] skip flaky suite (#66976) --- x-pack/test/accessibility/apps/home.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 1f05ff676e3a0..fe698acec322a 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -12,7 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const globalNav = getService('globalNav'); - describe('Kibana Home', () => { + // FLAKY: https://github.com/elastic/kibana/issues/66976 + describe.skip('Kibana Home', () => { before(async () => { await PageObjects.common.navigateToApp('home'); }); From 87c34cf10f9edd7c446312190fb5e3051962103a Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 29 May 2020 13:50:33 -0700 Subject: [PATCH 25/38] [DOCS] Identifies cloud settings for ML (#67573) --- docs/settings/ml-settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 24e38e73bca9b..1753e1fdecb95 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -13,7 +13,7 @@ enabled by default. [cols="2*<"] |=== -| `xpack.ml.enabled` +| `xpack.ml.enabled` {ess-icon} | Set to `true` (default) to enable {kib} {ml-features}. + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} From 402018856eea66a88785256d1a603a011795613a Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 29 May 2020 14:21:14 -0700 Subject: [PATCH 26/38] [kbn/optimizer] update public path before imports (#67561) Co-authored-by: spalger --- .../mock_repo/plugins/bar/public/index.scss | 3 +++ .../mock_repo/plugins/bar/public/index.ts | 1 + .../basic_optimization.test.ts.snap | 6 ++--- .../basic_optimization.test.ts | 19 ++++++++++----- .../src/worker/webpack.config.ts | 12 +++++++--- packages/kbn-ui-shared-deps/package.json | 2 ++ .../kbn-ui-shared-deps/public_path_loader.js | 10 +++++++- .../public_path_module_creator.js | 24 +++++++++++++++++++ 8 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss create mode 100644 packages/kbn-ui-shared-deps/public_path_module_creator.js diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss new file mode 100644 index 0000000000000..563d20e99ce82 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss @@ -0,0 +1,3 @@ +body { + color: green; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 817c4796562e8..7ddd10f4a388f 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -18,6 +18,7 @@ */ import './legacy/styles.scss'; +import './index.scss'; import { fooLibFn } from '../../foo/public/index'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 8bf5eb72523ff..2814ab32017d2 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -55,8 +55,8 @@ OptimizerConfig { } `; -exports[`prepares assets for distribution: 1 async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],[,function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}]]);"`; +exports[`prepares assets for distribution: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i value.split(REPO_ROOT).join('').replace(/\\/g, '/'), + test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT), +}); const log = new ToolingLog({ level: 'error', @@ -129,13 +132,14 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const foo = config.bundles.find((b) => b.id === 'foo')!; expect(foo).toBeTruthy(); foo.cache.refresh(); - expect(foo.cache.getModuleCount()).toBe(4); + expect(foo.cache.getModuleCount()).toBe(5); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -143,14 +147,15 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(bar).toBeTruthy(); bar.cache.refresh(); expect(bar.cache.getModuleCount()).toBe( - // code + styles + style/css-loader runtimes - 15 + // code + styles + style/css-loader runtimes + public path updater + 21 ); expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, @@ -159,6 +164,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); }); @@ -207,7 +213,7 @@ it('prepares assets for distribution', async () => { expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( 'plugins/foo/target/public/1.plugin.js', - '1 async bundle' + 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); }); @@ -217,6 +223,7 @@ it('prepares assets for distribution', async () => { */ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabel: string) => { const raw = Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, filePath), 'utf8'); + expect(raw).toMatchSnapshot(snapshotLabel); // Verify the brotli variant matches diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 0c9a5b0a75687..763f1d515804f 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -17,6 +17,7 @@ * under the License. */ +import Fs from 'fs'; import Path from 'path'; import normalizePath from 'normalize-path'; @@ -86,12 +87,17 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { } export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { + const extensions = ['.js', '.ts', '.tsx', '.json']; + const entryExtension = extensions.find((ext) => + Fs.existsSync(Path.resolve(bundle.contextDir, bundle.entry) + ext) + ); + const commonConfig: webpack.Configuration = { node: { fs: 'empty' }, context: bundle.contextDir, cache: true, entry: { - [bundle.id]: bundle.entry, + [bundle.id]: `${bundle.entry}${entryExtension}`, }, devtool: worker.dist ? false : '#cheap-source-map', @@ -144,7 +150,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { rules: [ { - include: Path.join(bundle.contextDir, bundle.entry), + include: [`${Path.resolve(bundle.contextDir, bundle.entry)}${entryExtension}`], loader: UiSharedDeps.publicPathLoader, options: { key: bundle.id, @@ -292,7 +298,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { }, resolve: { - extensions: ['.js', '.ts', '.tsx', '.json'], + extensions, mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index d34fe3624ab26..4e6bec92a65e4 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -35,6 +35,8 @@ "devDependencies": { "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "loader-utils": "^1.2.3", + "val-loader": "^1.1.1", "css-loader": "^3.4.2", "del": "^5.1.0", "webpack": "^4.41.5" diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js index fceebd6d6b3a1..cdebdcb4f0422 100644 --- a/packages/kbn-ui-shared-deps/public_path_loader.js +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -17,7 +17,15 @@ * under the License. */ +const Qs = require('querystring'); +const { stringifyRequest } = require('loader-utils'); + +const VAL_LOADER = require.resolve('val-loader'); +const MODULE_CREATOR = require.resolve('./public_path_module_creator'); + module.exports = function (source) { const options = this.query; - return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; + const valOpts = Qs.stringify({ key: options.key }); + const req = `${VAL_LOADER}?${valOpts}!${MODULE_CREATOR}`; + return `import ${stringifyRequest(this, req)};${source}`; }; diff --git a/packages/kbn-ui-shared-deps/public_path_module_creator.js b/packages/kbn-ui-shared-deps/public_path_module_creator.js new file mode 100644 index 0000000000000..1cb9989432178 --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_module_creator.js @@ -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. + */ + +module.exports = function ({ key }) { + return { + code: `__webpack_public_path__ = window.__kbnPublicPath__['${key}']`, + }; +}; From 3c40b97794640afda4120c2f91c62ac660cf41aa Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 29 May 2020 15:24:04 -0700 Subject: [PATCH 27/38] [DOCS] Link machine learning settings to advanced settings (#67572) --- docs/settings/ml-settings.asciidoc | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 1753e1fdecb95..83443636fa633 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -6,7 +6,7 @@ ++++ You do not need to configure any settings to use {kib} {ml-features}. They are -enabled by default. +enabled by default. [[general-ml-settings-kb]] ==== General {ml} settings @@ -23,13 +23,7 @@ enabled by default. |=== -[[data-visualizer-settings]] -==== {data-viz} settings +[[advanced-ml-settings-kb]] +==== Advanced {ml} settings -[cols="2*<"] -|=== -| `xpack.ml.file_data_visualizer.max_file_size` - | Sets the file size limit when importing data in the {data-viz}. The default - value is `100MB`. The highest supported value for this setting is `1GB`. - -|=== +Refer to <>. \ No newline at end of file From 39902870c8b6756e4f897633defbb30fcb6fd382 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 29 May 2020 15:31:17 -0700 Subject: [PATCH 28/38] [Reporting]: Move router + license checks to new platform (#66331) * WIP: Move routes to new API, license and other checks inbound * Move license checks over to np licensing observable * Fix license checks + remove older modules * Fixing check_license tests, move to TS/Jest * Fix licensing setup for mocks * Move job.test.ts over to np * WIP: move user checks to higher-order func * Move more handler logic over to Response factory vs Boom * Major refactor to consolidate types, remove facades, and udpate helpers * Fix validation for dates in immediate exports * Linter fix on check license test * Fix job generation tests * Move deps => setupDeps * fix api test * fix jobs test * authorized_user_pre_routing and tests * Fixing duplicate identifiers * Fix licensing implementation changes * WIP: Moving license over to async/observables * Fix disabled-security case * finish auth_user_pre_routing cleanup - no more license check * WIP: Fixing final api tests * Trying to get schema differences in alignment * Reverting back to previous generation handler * Fix final API tests * Final API test fixes, few more hardening tests and better error messages * Simplify lower-level module implementation (core only interface) + test updates * Push some core logic into plugin * Move some core logic up to plugin * Marking private setupDeps + downstream fixes * revert logger as a param Co-authored-by: Timothy Sullivan --- .../plugins/reporting/common/constants.ts | 1 + .../export_types/csv/server/create_job.ts | 19 +- .../server/create_job/create_job.ts | 18 +- .../server/execute_job.ts | 22 +- .../server/lib/generate_csv.ts | 14 +- .../server/lib/generate_csv_search.ts | 34 +- .../server/lib/get_filters.ts | 2 +- .../server/lib/get_job_params_from_request.ts | 12 +- .../png/server/create_job/index.ts | 21 +- .../printable_pdf/server/create_job/index.ts | 19 +- .../legacy/plugins/reporting/server/core.ts | 86 ++- .../legacy/plugins/reporting/server/legacy.ts | 3 +- .../server/lib/__tests__/check_license.js | 147 ----- .../server/lib/check_license.test.ts | 192 ++++++ .../reporting/server/lib/check_license.ts | 56 +- .../reporting/server/lib/enqueue_job.ts | 24 +- .../plugins/reporting/server/lib/get_user.ts | 13 +- .../plugins/reporting/server/lib/index.ts | 2 +- .../reporting/server/lib/jobs_query.ts | 31 +- .../legacy/plugins/reporting/server/plugin.ts | 33 +- .../server/routes/generate_from_jobparams.ts | 125 ++-- .../routes/generate_from_savedobject.ts | 81 +-- .../generate_from_savedobject_immediate.ts | 114 ++-- .../server/routes/generation.test.ts | 274 +++++---- .../reporting/server/routes/generation.ts | 84 +-- .../plugins/reporting/server/routes/index.ts | 14 +- .../reporting/server/routes/jobs.test.ts | 564 +++++++++--------- .../plugins/reporting/server/routes/jobs.ts | 330 +++++----- .../lib/authorized_user_pre_routing.test.js | 175 ------ .../lib/authorized_user_pre_routing.test.ts | 134 +++++ .../routes/lib/authorized_user_pre_routing.ts | 74 ++- .../server/routes/lib/get_document_payload.ts | 14 +- .../server/routes/lib/job_response_handler.ts | 77 ++- .../routes/lib/make_request_facade.test.ts | 62 -- .../server/routes/lib/make_request_facade.ts | 32 - .../lib/reporting_feature_pre_routing.ts | 36 -- .../routes/lib/route_config_factories.ts | 130 ---- .../reporting/server/routes/types.d.ts | 13 +- .../legacy/plugins/reporting/server/types.ts | 21 +- .../create_mock_reportingplugin.ts | 20 +- .../test_helpers/create_mock_server.ts | 31 +- x-pack/plugins/reporting/kibana.json | 3 +- .../reporting/csv_job_params.ts | 2 +- 43 files changed, 1492 insertions(+), 1667 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js create mode 100644 x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js create mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index f30a7cc87f318..48483c79d1af2 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -27,6 +27,7 @@ export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/pdf', CONTENT_TYPE_CSV, 'image/png', + 'text/plain', ]; // See: diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts index 8320cd05aa2e7..c76b4afe727da 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ReportingCore } from '../../../server'; import { cryptoFactory } from '../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../server/types'; import { JobParamsDiscoverCsv } from '../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); + const setupDeps = reporting.getPluginSetupDeps(); return async function createJob( jobParams: JobParamsDiscoverCsv, - headers: ConditionalHeaders['headers'], - request: RequestFacade + context: RequestHandlerContext, + request: KibanaRequest ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(request.headers); - const savedObjectsClient = request.getSavedObjectsClient(); + const savedObjectsClient = context.core.savedObjects.client; const indexPatternSavedObject = await savedObjectsClient.get( 'index-pattern', jobParams.indexPatternId! @@ -36,7 +33,7 @@ export const createJobFactory: CreateJobFactory = ( jobParams: JobParamsType, - headers: Record, - req: RequestFacade + headers: KibanaRequest['headers'], + context: RequestHandlerContext, + req: KibanaRequest ) => Promise<{ type: string | null; title: string; @@ -46,21 +48,21 @@ export const createJobFactory: CreateJobFactory { const { savedObjectType, savedObjectId } = jobParams; const serializedEncryptedHeaders = await crypto.encrypt(headers); - const client = req.getSavedObjectsClient(); const { panel, title, visType }: VisData = await Promise.resolve() - .then(() => client.get(savedObjectType, savedObjectId)) + .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) .then(async (savedObject: SavedObject) => { const { attributes, references } = savedObject; const { kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, } = attributes as SavedSearchObjectAttributesJSON; - const { timerange } = req.payload as { timerange: TimeRangeParams }; + const { timerange } = req.body as { timerange: TimeRangeParams }; if (!kibanaSavedObjectMetaJSON) { throw new Error('Could not parse saved object data!'); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 5761a98ed160c..4ef7b8514b363 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,15 +5,11 @@ */ import { i18n } from '@kbn/i18n'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { ReportingCore } from '../../../server'; import { cryptoFactory, LevelLogger } from '../../../server/lib'; -import { - ExecuteJobFactory, - JobDocOutput, - JobDocPayload, - RequestFacade, -} from '../../../server/types'; +import { ExecuteJobFactory, JobDocOutput, JobDocPayload } from '../../../server/types'; import { CsvResultFromSearch } from '../../csv/types'; import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; import { createGenerateCsv } from './lib'; @@ -25,7 +21,8 @@ import { createGenerateCsv } from './lib'; export type ImmediateExecuteFn = ( jobId: null, job: JobDocPayload, - request: RequestFacade + context: RequestHandlerContext, + req: KibanaRequest ) => Promise; export const executeJobFactory: ExecuteJobFactory { // There will not be a jobID for "immediate" generation. // jobID is only for "queued" jobs @@ -58,10 +56,11 @@ export const executeJobFactory: ExecuteJobFactory; @@ -103,6 +102,7 @@ export const executeJobFactory: ExecuteJobFactory { }; export async function generateCsvSearch( - req: RequestFacade, reporting: ReportingCore, - logger: LevelLogger, + context: RequestHandlerContext, + req: KibanaRequest, searchPanel: SearchPanel, - jobParams: JobParamsDiscoverCsv + jobParams: JobParamsDiscoverCsv, + logger: LevelLogger ): Promise { - const savedObjectsClient = await reporting.getSavedObjectsClient( - KibanaRequest.from(req.getRawRequest()) - ); + const savedObjectsClient = context.core.savedObjects.client; const { indexPatternSavedObjectId, timerange } = searchPanel; - const savedSearchObjectAttr = searchPanel.attributes as SavedSearchObjectAttributes; + const savedSearchObjectAttr = searchPanel.attributes; const { indexPatternSavedObject } = await getDataSource( savedObjectsClient, indexPatternSavedObjectId @@ -153,9 +149,7 @@ export async function generateCsvSearch( const config = reporting.getConfig(); const elasticsearch = await reporting.getElasticsearchService(); - const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( - KibanaRequest.from(req.getRawRequest()) - ); + const { callAsCurrentUser } = elasticsearch.dataClient.asScoped(req); const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts index 071427f4dab64..4695bbd922581 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts @@ -22,7 +22,7 @@ export function getFilters( let timezone: string | null; if (indexPatternTimeField) { - if (!timerange) { + if (!timerange || !timerange.min || !timerange.max) { throw badRequest( `Time range params are required for index pattern [${indexPatternId}], using time field [${indexPatternTimeField}]` ); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts index 57d74ee0e1383..5aed02c10b961 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestFacade } from '../../../../server/types'; +import { KibanaRequest } from 'src/core/server'; import { JobParamsPanelCsv, JobParamsPostPayloadPanelCsv } from '../../types'; export function getJobParamsFromRequest( - request: RequestFacade, + request: KibanaRequest, opts: { isImmediate: boolean } ): JobParamsPanelCsv { - const { savedObjectType, savedObjectId } = request.params; - const { timerange, state } = request.payload as JobParamsPostPayloadPanelCsv; + const { savedObjectType, savedObjectId } = request.params as { + savedObjectType: string; + savedObjectId: string; + }; + const { timerange, state } = request.body as JobParamsPostPayloadPanelCsv; + const post = timerange || state ? { timerange, state } : undefined; return { diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts index b19513de29eee..ab492c21256eb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts @@ -5,28 +5,23 @@ */ import { validateUrls } from '../../../../common/validate_urls'; -import { ReportingCore } from '../../../../server'; import { cryptoFactory } from '../../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../server/types'; import { JobParamsPNG } from '../../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJob( - { objectType, title, relativeUrl, browserTimezone, layout }: JobParamsPNG, - headers: ConditionalHeaders['headers'], - request: RequestFacade + { objectType, title, relativeUrl, browserTimezone, layout }, + context, + req ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); validateUrls([relativeUrl]); @@ -37,7 +32,7 @@ export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJobFn( { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, - headers: ConditionalHeaders['headers'], - request: RequestFacade + context, + req ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); validateUrls(relativeUrls); return { - basePath: request.getBasePath(), + basePath: setupDeps.basePath(req), browserTimezone, forceNow: new Date().toISOString(), headers: serializedEncryptedHeaders, diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/legacy/plugins/reporting/server/core.ts index 8fb948a253c16..b89ef9e06b961 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/legacy/plugins/reporting/server/core.ts @@ -5,33 +5,35 @@ */ import * as Rx from 'rxjs'; -import { first, mapTo } from 'rxjs/operators'; +import { first, mapTo, map } from 'rxjs/operators'; import { ElasticsearchServiceSetup, KibanaRequest, - SavedObjectsClient, SavedObjectsServiceStart, UiSettingsServiceStart, + IRouter, + SavedObjectsClientContract, + BasePath, } from 'src/core/server'; -import { ReportingPluginSpecOptions } from '../'; -// @ts-ignore no module definition -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; -import { PLUGIN_ID } from '../common/constants'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; import { screenshotsObservableFactory } from '../export_types/common/lib/screenshots'; -import { ServerFacade } from '../server/types'; +import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; -import { checkLicenseFactory, getExportTypesRegistry, LevelLogger } from './lib'; +import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { EnqueueJobFn } from './lib/enqueue_job'; -import { registerRoutes } from './routes'; -import { ReportingSetupDeps } from './types'; -interface ReportingInternalSetup { +export interface ReportingInternalSetup { browserDriverFactory: HeadlessChromiumDriverFactory; elasticsearch: ElasticsearchServiceSetup; + licensing: LicensingPluginSetup; + basePath: BasePath['get']; + router: IRouter; + security: SecurityPluginSetup; } + interface ReportingInternalStart { enqueueJob: EnqueueJobFn; esqueue: ESQueueInstance; @@ -46,30 +48,10 @@ export class ReportingCore { private readonly pluginStart$ = new Rx.ReplaySubject(); private exportTypesRegistry = getExportTypesRegistry(); - constructor(private logger: LevelLogger, private config: ReportingConfig) {} - - legacySetup( - xpackMainPlugin: XPackMainPlugin, - reporting: ReportingPluginSpecOptions, - __LEGACY: ServerFacade, - plugins: ReportingSetupDeps - ) { - // legacy plugin status - mirrorPluginStatus(xpackMainPlugin, reporting); - - // legacy license check - const checkLicense = checkLicenseFactory(this.exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // legacy routes - registerRoutes(this, __LEGACY, plugins, this.logger); - } + constructor(private config: ReportingConfig) {} public pluginSetup(reportingSetupDeps: ReportingInternalSetup) { + this.pluginSetupDeps = reportingSetupDeps; this.pluginSetup$.next(reportingSetupDeps); } @@ -96,23 +78,35 @@ export class ReportingCore { return (await this.getPluginStartDeps()).enqueueJob; } - public getConfig() { + public async getLicenseInfo() { + const { licensing } = this.getPluginSetupDeps(); + return await licensing.license$ + .pipe( + map((license) => checkLicense(this.getExportTypesRegistry(), license)), + first() + ) + .toPromise(); + } + + public getConfig(): ReportingConfig { return this.config; } - public async getScreenshotsObservable() { - const { browserDriverFactory } = await this.getPluginSetupDeps(); + + public getScreenshotsObservable(): ScreenshotsObservableFn { + const { browserDriverFactory } = this.getPluginSetupDeps(); return screenshotsObservableFactory(this.config.get('capture'), browserDriverFactory); } + public getPluginSetupDeps() { + if (!this.pluginSetupDeps) { + throw new Error(`"pluginSetupDeps" dependencies haven't initialized yet`); + } + return this.pluginSetupDeps; + } + /* * Outside dependencies */ - private async getPluginSetupDeps() { - if (this.pluginSetupDeps) { - return this.pluginSetupDeps; - } - return await this.pluginSetup$.pipe(first()).toPromise(); - } private async getPluginStartDeps() { if (this.pluginStartDeps) { @@ -122,15 +116,15 @@ export class ReportingCore { } public async getElasticsearchService() { - return (await this.getPluginSetupDeps()).elasticsearch; + return this.getPluginSetupDeps().elasticsearch; } public async getSavedObjectsClient(fakeRequest: KibanaRequest) { const { savedObjects } = await this.getPluginStartDeps(); - return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClient; + return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClientContract; } - public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClient) { + public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClientContract) { const { uiSettings: uiSettingsService } = await this.getPluginStartDeps(); const scopedUiSettingsService = uiSettingsService.asScopedToClient(savedObjectsClient); return scopedUiSettingsService; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts index 37272325b97d0..14abd53cc83d9 100644 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -7,8 +7,8 @@ import { Legacy } from 'kibana'; import { take } from 'rxjs/operators'; import { PluginInitializerContext } from 'src/core/server'; -import { ReportingPluginSpecOptions } from '../'; import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; +import { ReportingPluginSpecOptions } from '../'; import { PluginsSetup } from '../../../../plugins/reporting/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { buildConfig } from './config'; @@ -42,6 +42,7 @@ export const legacyInit = async ( server.newPlatform.coreContext as PluginInitializerContext, buildConfig(coreSetup, server, reportingConfig) ); + await pluginInstance.setup(coreSetup, { elasticsearch: coreSetup.elasticsearch, licensing: server.newPlatform.setup.plugins.licensing as LicensingPluginSetup, diff --git a/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js deleted file mode 100644 index 294a0df56756e..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js +++ /dev/null @@ -1,147 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicenseFactory } from '../check_license'; - -describe('check_license', function () { - let mockLicenseInfo; - let checkLicense; - - beforeEach(() => { - mockLicenseInfo = {}; - checkLicense = checkLicenseFactory({ - getAll: () => [ - { - id: 'test', - name: 'Test Export Type', - jobType: 'testJobType', - }, - ], - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(false); - }); - - it('should set test.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(true); - }); - - it('should set test.enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(true); - }); - - it('should set management.jobTypes to contain testJobType', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.contain('testJobType'); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(false); - }); - - it('should set test.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => false)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(false); - }); - - it('should set test.showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(false); - }); - - it('should set management.jobTypes to an empty array', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be.an(Array); - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.have.length(0); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts b/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts new file mode 100644 index 0000000000000..366a8d94286f1 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { checkLicense } from './check_license'; +import { ILicense } from '../../../../../plugins/licensing/server'; +import { ExportTypesRegistry } from './export_types_registry'; + +describe('check_license', () => { + let exportTypesRegistry: ExportTypesRegistry; + let license: ILicense; + + beforeEach(() => { + exportTypesRegistry = ({ + getAll: () => [], + } as unknown) as ExportTypesRegistry; + }); + + describe('license information is not ready', () => { + beforeEach(() => { + exportTypesRegistry = ({ + getAll: () => [{ id: 'csv' }], + } as unknown) as ExportTypesRegistry; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.showLinks).toEqual(true); + }); + + it('should set csv.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, undefined).csv.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.enableLinks).toEqual(false); + }); + + it('should set csv.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, undefined).csv.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.jobTypes).toEqual(undefined); + }); + }); + + describe('license information is not available', () => { + beforeEach(() => { + license = { + type: undefined, + } as ILicense; + exportTypesRegistry = ({ + getAll: () => [{ id: 'csv' }], + } as unknown) as ExportTypesRegistry; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set csv.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).csv.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(false); + }); + + it('should set csv.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).csv.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + + describe('license information is available', () => { + beforeEach(() => { + license = {} as ILicense; + }); + + describe('& license is > basic', () => { + beforeEach(() => { + license.type = 'gold'; + exportTypesRegistry = ({ + getAll: () => [{ id: 'pdf', validLicenses: ['gold'], jobType: 'printable_pdf' }], + } as unknown) as ExportTypesRegistry; + }); + + describe('& license is active', () => { + beforeEach(() => (license.isActive = true)); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should setpdf.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(true); + }); + + it('should setpdf.enableLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.enableLinks).toEqual(true); + }); + + it('should set management.jobTypes to contain testJobType', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toContain( + 'printable_pdf' + ); + }); + }); + + describe('& license is expired', () => { + beforeEach(() => { + license.isActive = false; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set pdf.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(false); + }); + + it('should set pdf.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + }); + + describe('& license is basic', () => { + beforeEach(() => { + license.type = 'basic'; + exportTypesRegistry = ({ + getAll: () => [{ id: 'pdf', validLicenses: ['gold'], jobType: 'printable_pdf' }], + } as unknown) as ExportTypesRegistry; + }); + + describe('& license is active', () => { + beforeEach(() => { + license.isActive = true; + }); + + it('should set management.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(false); + }); + + it('should set test.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(false); + }); + + it('should set management.jobTypes to an empty array', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual([]); + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toHaveLength(0); + }); + }); + + describe('& license is expired', () => { + beforeEach(() => { + license.isActive = false; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set test.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts index 80cf315539441..1b4eeaa0bae3e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { XPackInfo } from '../../../xpack_main/server/lib/xpack_info'; -import { XPackInfoLicense } from '../../../xpack_main/server/lib/xpack_info_license'; +import { ILicense } from '../../../../../plugins/licensing/server'; import { ExportTypeDefinition } from '../types'; import { ExportTypesRegistry } from './export_types_registry'; -interface LicenseCheckResult { +export interface LicenseCheckResult { showLinks: boolean; enableLinks: boolean; message?: string; + jobTypes?: string[]; } const messages = { getUnavailable: () => { return 'You cannot use Reporting because license information is not available at this time.'; }, - getExpired: (license: XPackInfoLicense) => { - return `You cannot use Reporting because your ${license.getType()} license has expired.`; + getExpired: (license: ILicense) => { + return `You cannot use Reporting because your ${license.type} license has expired.`; }, }; @@ -29,8 +29,8 @@ const makeManagementFeature = ( ) => { return { id: 'management', - checkLicense: (license: XPackInfoLicense | null) => { - if (!license) { + checkLicense: (license?: ILicense) => { + if (!license || !license.type) { return { showLinks: true, enableLinks: false, @@ -38,7 +38,7 @@ const makeManagementFeature = ( }; } - if (!license.isActive()) { + if (!license.isActive) { return { showLinks: true, enableLinks: false, @@ -47,7 +47,7 @@ const makeManagementFeature = ( } const validJobTypes = exportTypes - .filter((exportType) => license.isOneOf(exportType.validLicenses)) + .filter((exportType) => exportType.validLicenses.includes(license.type || '')) .map((exportType) => exportType.jobType); return { @@ -64,8 +64,8 @@ const makeExportTypeFeature = ( ) => { return { id: exportType.id, - checkLicense: (license: XPackInfoLicense | null) => { - if (!license) { + checkLicense: (license?: ILicense) => { + if (!license || !license.type) { return { showLinks: true, enableLinks: false, @@ -73,17 +73,15 @@ const makeExportTypeFeature = ( }; } - if (!license.isOneOf(exportType.validLicenses)) { + if (!exportType.validLicenses.includes(license.type)) { return { showLinks: false, enableLinks: false, - message: `Your ${license.getType()} license does not support ${ - exportType.name - } Reporting. Please upgrade your license.`, + message: `Your ${license.type} license does not support ${exportType.name} Reporting. Please upgrade your license.`, }; } - if (!license.isActive()) { + if (!license.isActive) { return { showLinks: true, enableLinks: false, @@ -99,18 +97,18 @@ const makeExportTypeFeature = ( }; }; -export function checkLicenseFactory(exportTypesRegistry: ExportTypesRegistry) { - return function checkLicense(xpackInfo: XPackInfo) { - const license = xpackInfo === null || !xpackInfo.isAvailable() ? null : xpackInfo.license; - const exportTypes = Array.from(exportTypesRegistry.getAll()); - const reportingFeatures = [ - ...exportTypes.map(makeExportTypeFeature), - makeManagementFeature(exportTypes), - ]; +export function checkLicense( + exportTypesRegistry: ExportTypesRegistry, + license: ILicense | undefined +) { + const exportTypes = Array.from(exportTypesRegistry.getAll()); + const reportingFeatures = [ + ...exportTypes.map(makeExportTypeFeature), + makeManagementFeature(exportTypes), + ]; - return reportingFeatures.reduce((result, feature) => { - result[feature.id] = feature.checkLicense(license); - return result; - }, {} as Record); - }; + return reportingFeatures.reduce((result, feature) => { + result[feature.id] = feature.checkLicense(license); + return result; + }, {} as Record); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 8ffb99f7a14c8..6367c8a1da98a 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,8 +5,9 @@ */ import { EventEmitter } from 'events'; -import { get } from 'lodash'; -import { ConditionalHeaders, ESQueueCreateJobFn, RequestFacade } from '../../server/types'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../../../plugins/security/server'; +import { ESQueueCreateJobFn } from '../../server/types'; import { ReportingCore } from '../core'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; @@ -29,9 +30,9 @@ export type Job = EventEmitter & { export type EnqueueJobFn = ( exportTypeId: string, jobParams: JobParamsType, - user: string, - headers: Record, - request: RequestFacade + user: AuthenticatedUser | null, + context: RequestHandlerContext, + request: KibanaRequest ) => Promise; export function enqueueJobFactory( @@ -42,18 +43,17 @@ export function enqueueJobFactory( const queueTimeout = config.get('queue', 'timeout'); const browserType = config.get('capture', 'browser', 'type'); const maxAttempts = config.get('capture', 'maxAttempts'); - const logger = parentLogger.clone(['queue-job']); return async function enqueueJob( exportTypeId: string, jobParams: JobParamsType, - user: string, - headers: ConditionalHeaders['headers'], - request: RequestFacade + user: AuthenticatedUser | null, + context: RequestHandlerContext, + request: KibanaRequest ): Promise { type CreateJobFn = ESQueueCreateJobFn; - + const username = user ? user.username : false; const esqueue = await reporting.getEsqueue(); const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); @@ -62,11 +62,11 @@ export function enqueueJobFactory( } const createJob = exportType.createJobFactory(reporting, logger) as CreateJobFn; - const payload = await createJob(jobParams, headers, request); + const payload = await createJob(jobParams, context, request); const options = { timeout: queueTimeout, - created_by: get(user, 'username', false), + created_by: username, browser_type: browserType, max_attempts: maxAttempts, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts index 8e8b1c83d5a40..164ffc5742d04 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts @@ -4,16 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { SecurityPluginSetup } from '../../../../../plugins/security/server'; import { KibanaRequest } from '../../../../../../src/core/server'; -import { ReportingSetupDeps } from '../types'; -import { LevelLogger } from './level_logger'; -export function getUserFactory(security: ReportingSetupDeps['security'], logger: LevelLogger) { - /* - * Legacy.Request because this is called from routing middleware - */ - return async (request: Legacy.Request) => { - return security?.authc.getCurrentUser(KibanaRequest.from(request)) ?? null; +export function getUserFactory(security?: SecurityPluginSetup) { + return (request: KibanaRequest) => { + return security?.authc.getCurrentUser(request) ?? null; }; } diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index 2a8fa45b6fcef..0e9c49b170887 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -5,7 +5,7 @@ */ export { LevelLogger } from './level_logger'; -export { checkLicenseFactory } from './check_license'; +export { checkLicense } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 1abf58c29b481..5153fd0f4e5b8 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; +import { AuthenticatedUser } from '../../../../../plugins/security/server'; import { ReportingConfig } from '../'; import { JobSource } from '../types'; @@ -40,6 +40,8 @@ interface CountAggResult { count: number; } +const getUsername = (user: AuthenticatedUser | null) => (user ? user.username : false); + export function jobsQueryFactory( config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup @@ -47,10 +49,6 @@ export function jobsQueryFactory( const index = config.get('index'); const { callAsInternalUser } = elasticsearch.adminClient; - function getUsername(user: any) { - return get(user, 'username', false); - } - function execQuery(queryType: string, body: QueryBody) { const defaultBody: Record = { search: { @@ -82,9 +80,14 @@ export function jobsQueryFactory( } return { - list(jobTypes: string[], user: any, page = 0, size = defaultSize, jobIds: string[] | null) { + list( + jobTypes: string[], + user: AuthenticatedUser | null, + page = 0, + size = defaultSize, + jobIds: string[] | null + ) { const username = getUsername(user); - const body: QueryBody = { size, from: size * page, @@ -108,9 +111,8 @@ export function jobsQueryFactory( return getHits(execQuery('search', body)); }, - count(jobTypes: string[], user: any) { + count(jobTypes: string[], user: AuthenticatedUser | null) { const username = getUsername(user); - const body: QueryBody = { query: { constant_score: { @@ -129,9 +131,12 @@ export function jobsQueryFactory( }); }, - get(user: any, id: string, opts: GetOpts = {}): Promise | void> { + get( + user: AuthenticatedUser | null, + id: string, + opts: GetOpts = {} + ): Promise | void> { if (!id) return Promise.resolve(); - const username = getUsername(user); const body: QueryBody = { @@ -164,14 +169,12 @@ export function jobsQueryFactory( const query = { id, index: deleteIndex }; return callAsInternalUser('delete', query); } catch (error) { - const wrappedError = new Error( + throw new Error( i18n.translate('xpack.reporting.jobsQuery.deleteError', { defaultMessage: 'Could not delete the report: {error}', values: { error: error.message }, }) ); - - throw Boom.boomify(wrappedError, { statusCode: error.status }); } }, }; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index 78c2ce5b9b106..5a407ad3e4c4a 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -8,10 +8,13 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core import { createBrowserDriverFactory } from './browsers'; import { ReportingConfig } from './config'; import { ReportingCore } from './core'; +import { registerRoutes } from './routes'; import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; import { registerReportingUsageCollector } from './usage'; +// @ts-ignore no module definition +import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; export class ReportingPlugin implements Plugin { @@ -22,24 +25,34 @@ export class ReportingPlugin constructor(context: PluginInitializerContext, config: ReportingConfig) { this.config = config; this.logger = new LevelLogger(context.logger.get('reporting')); - this.reportingCore = new ReportingCore(this.logger, this.config); + this.reportingCore = new ReportingCore(this.config); } public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { const { config } = this; - const { elasticsearch, __LEGACY } = plugins; + const { elasticsearch, __LEGACY, licensing, security } = plugins; + const router = core.http.createRouter(); + const basePath = core.http.basePath.get; + const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); // required for validations :( - runValidations(config, elasticsearch, browserDriverFactory, this.logger); + // legacy plugin status + mirrorPluginStatus(xpackMainLegacy, reportingLegacy); - const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - this.reportingCore.legacySetup(xpackMainLegacy, reportingLegacy, __LEGACY, plugins); + const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); + const deps = { + browserDriverFactory, + elasticsearch, + licensing, + basePath, + router, + security, + }; - // Register a function with server to manage the collection of usage stats - registerReportingUsageCollector(this.reportingCore, plugins); + runValidations(config, elasticsearch, browserDriverFactory, this.logger); - // regsister setup internals - this.reportingCore.pluginSetup({ browserDriverFactory, elasticsearch }); + this.reportingCore.pluginSetup(deps); + registerReportingUsageCollector(this.reportingCore, plugins); + registerRoutes(this.reportingCore, this.logger); return {}; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 3f79d51382a81..2a12a64d67a35 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -4,73 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; -import Joi from 'joi'; -import { Legacy } from 'kibana'; import rison from 'rison-node'; +import { schema } from '@kbn/config-schema'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { HandlerErrorFunction, HandlerFunction } from './types'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { - GetRouteConfigFactoryFn, - getRouteConfigFactoryReportingPre, - RouteConfigFactory, -} from './lib/route_config_factories'; -import { HandlerErrorFunction, HandlerFunction, ReportingResponseToolkit } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; export function registerGenerateFromJobParams( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, handler: HandlerFunction, - handleError: HandlerErrorFunction, - logger: Logger + handleError: HandlerErrorFunction ) { - const config = reporting.getConfig(); - const getRouteConfig = () => { - const getOriginalRouteConfig: GetRouteConfigFactoryFn = getRouteConfigFactoryReportingPre( - config, - plugins, - logger - ); - const routeConfigFactory: RouteConfigFactory = getOriginalRouteConfig( - ({ params: { exportType } }) => exportType - ); + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; - return { - ...routeConfigFactory, + router.post( + { + path: `${BASE_GENERATE}/{exportType}`, validate: { - params: Joi.object({ - exportType: Joi.string().required(), - }).required(), - payload: Joi.object({ - jobParams: Joi.string().optional().default(null), - }).allow(null), // allow optional payload - query: Joi.object({ - jobParams: Joi.string().default(null), - }).default(), + params: schema.object({ + exportType: schema.string({ minLength: 2 }), + }), + body: schema.nullable( + schema.object({ + jobParams: schema.maybe(schema.string()), + }) + ), + query: schema.nullable( + schema.object({ + jobParams: schema.string({ + defaultValue: '', + }), + }) + ), }, - }; - }; - - // generate report - server.route({ - path: `${BASE_GENERATE}/{exportType}`, - method: 'POST', - options: getRouteConfig(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); + }, + userHandler(async (user, context, req, res) => { let jobParamsRison: string | null; - if (request.payload) { - const { jobParams: jobParamsPayload } = request.payload as { jobParams: string }; + if (req.body) { + const { jobParams: jobParamsPayload } = req.body as { jobParams: string }; jobParamsRison = jobParamsPayload; } else { - const { jobParams: queryJobParams } = request.query as { jobParams: string }; + const { jobParams: queryJobParams } = req.query as { jobParams: string }; if (queryJobParams) { jobParamsRison = queryJobParams; } else { @@ -79,37 +59,46 @@ export function registerGenerateFromJobParams( } if (!jobParamsRison) { - throw boom.badRequest('A jobParams RISON string is required'); + return res.customError({ + statusCode: 400, + body: 'A jobParams RISON string is required in the querystring or POST body', + }); } - const { exportType } = request.params; + const { exportType } = req.params as { exportType: string }; let jobParams; - let response; + try { jobParams = rison.decode(jobParamsRison) as object | null; if (!jobParams) { - throw new Error('missing jobParams!'); + return res.customError({ + statusCode: 400, + body: 'Missing jobParams!', + }); } } catch (err) { - throw boom.badRequest(`invalid rison: ${jobParamsRison}`); + return res.customError({ + statusCode: 400, + body: `invalid rison: ${jobParamsRison}`, + }); } + try { - response = await handler(exportType, jobParams, legacyRequest, h); + return await handler(user, exportType, jobParams, context, req, res); } catch (err) { - throw handleError(exportType, err); + return handleError(res, err); } - return response; - }, - }); + }) + ); // Get route to generation endpoint: show error about GET method to user - server.route({ - path: `${BASE_GENERATE}/{p*}`, - method: 'GET', - handler: () => { - const err = boom.methodNotAllowed('GET is not allowed'); - err.output.headers.allow = 'POST'; - return err; + router.get( + { + path: `${BASE_GENERATE}/{p*}`, + validate: false, }, - }); + (context, req, res) => { + return res.customError({ statusCode: 405, body: 'GET is not allowed' }); + } + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts index 03a893d1abeb4..4bc143b911572 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; +import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; -import { - HandlerErrorFunction, - HandlerFunction, - QueuedJobPayload, - ReportingResponseToolkit, -} from './types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* * This function registers API Endpoints for queuing Reporting jobs. The API inputs are: @@ -31,22 +23,31 @@ import { */ export function registerGenerateCsvFromSavedObject( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, handleRoute: HandlerFunction, - handleRouteError: HandlerErrorFunction, - logger: Logger + handleRouteError: HandlerErrorFunction ) { - const config = reporting.getConfig(); - const routeOptions = getRouteOptionsCsv(config, plugins, logger); - - server.route({ - path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, - method: 'POST', - options: routeOptions, - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const requestFacade = makeRequestFacade(legacyRequest); - + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; + router.post( + { + path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, + validate: { + params: schema.object({ + savedObjectType: schema.string({ minLength: 2 }), + savedObjectId: schema.string({ minLength: 2 }), + }), + body: schema.object({ + state: schema.object({}), + timerange: schema.object({ + timezone: schema.string({ defaultValue: 'UTC' }), + min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + }), + }), + }, + }, + userHandler(async (user, context, req, res) => { /* * 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle * 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params @@ -54,19 +55,31 @@ export function registerGenerateCsvFromSavedObject( */ let result: QueuedJobPayload; try { - const jobParams = getJobParamsFromRequest(requestFacade, { isImmediate: false }); - result = await handleRoute(CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobParams, legacyRequest, h); // pass the original request because the handler will make the request facade on its own + const jobParams = getJobParamsFromRequest(req, { isImmediate: false }); + result = await handleRoute( + user, + CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobParams, + context, + req, + res + ); } catch (err) { - throw handleRouteError(CSV_FROM_SAVEDOBJECT_JOB_TYPE, err); + return handleRouteError(res, err); } if (get(result, 'source.job') == null) { - throw new Error( - `The Export handler is expected to return a result with job info! ${result}` - ); + return res.badRequest({ + body: `The Export handler is expected to return a result with job info! ${result}`, + }); } - return result; - }, - }); + return res.ok({ + body: result, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 22aebb05cdf49..8a6d4553dfa9c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -4,22 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseObject } from 'hapi'; -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; +import { HandlerErrorFunction } from './types'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFactory, executeJobFactory } from '../../export_types/csv_from_savedobject'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; -import { JobDocOutput, ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; -import { ReportingResponseToolkit } from './types'; - -type ResponseFacade = ResponseObject & { - isBoom: boolean; -}; +import { JobDocOutput } from '../types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: @@ -32,61 +26,77 @@ type ResponseFacade = ResponseObject & { */ export function registerGenerateCsvFromSavedObjectImmediate( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, + handleError: HandlerErrorFunction, parentLogger: Logger ) { - const config = reporting.getConfig(); - const routeOptions = getRouteOptionsCsv(config, plugins, parentLogger); + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: * - re-use the createJob function to build up es query config * - re-use the executeJob function to run the scan and scroll queries and capture the entire CSV in a result object. */ - server.route({ - path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, - method: 'POST', - options: routeOptions, - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); + router.post( + { + path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, + validate: { + params: schema.object({ + savedObjectType: schema.string({ minLength: 5 }), + savedObjectId: schema.string({ minLength: 5 }), + }), + body: schema.object({ + state: schema.object({}, { unknowns: 'allow' }), + timerange: schema.object({ + timezone: schema.string({ defaultValue: 'UTC' }), + min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + }), + }), + }, + }, + userHandler(async (user, context, req, res) => { const logger = parentLogger.clone(['savedobject-csv']); - const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); + const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); const createJobFn = createJobFactory(reporting, logger); const executeJobFn = await executeJobFactory(reporting, logger); // FIXME: does not "need" to be async - const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( - jobParams, - request.headers, - request - ); - const { - content_type: jobOutputContentType, - content: jobOutputContent, - size: jobOutputSize, - }: JobDocOutput = await executeJobFn(null, jobDocPayload, request); - logger.info(`Job output size: ${jobOutputSize} bytes`); + try { + const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( + jobParams, + req.headers, + context, + req + ); + const { + content_type: jobOutputContentType, + content: jobOutputContent, + size: jobOutputSize, + }: JobDocOutput = await executeJobFn(null, jobDocPayload, context, req); - /* - * ESQueue worker function defaults `content` to null, even if the - * executeJob returned undefined. - * - * This converts null to undefined so the value can be sent to h.response() - */ - if (jobOutputContent === null) { - logger.warn('CSV Job Execution created empty content result'); - } - const response = h - .response(jobOutputContent ? jobOutputContent : undefined) - .type(jobOutputContentType); + logger.info(`Job output size: ${jobOutputSize} bytes`); - // Set header for buffer download, not streaming - const { isBoom } = response as ResponseFacade; - if (isBoom == null) { - response.header('accept-ranges', 'none'); - } + /* + * ESQueue worker function defaults `content` to null, even if the + * executeJob returned undefined. + * + * This converts null to undefined so the value can be sent to h.response() + */ + if (jobOutputContent === null) { + logger.warn('CSV Job Execution created empty content result'); + } - return response; - }, - }); + return res.ok({ + body: jobOutputContent || '', + headers: { + 'content-type': jobOutputContentType, + 'accept-ranges': 'none', + }, + }); + } catch (err) { + return handleError(res, err); + } + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts index d767d37a477ab..fdde3253cf28e 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -4,140 +4,162 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ReportingConfig, ReportingCore } from '../'; -import { createMockReportingCore } from '../../test_helpers'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; import { registerJobGenerationRoutes } from './generation'; +import { createMockReportingCore } from '../../test_helpers'; +import { ReportingCore } from '..'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; +import { ExportTypeDefinition } from '../types'; +import { LevelLogger } from '../lib'; +import { of } from 'rxjs'; + +type setupServerReturn = UnwrapPromise>; + +describe('POST /api/reporting/generate', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + + const config = { + get: jest.fn().mockImplementation((...args) => { + const key = args.join('.'); + switch (key) { + case 'queue.indexInterval': + return 10000; + case 'queue.timeout': + return 10000; + case 'index': + return '.reporting'; + case 'queue.pollEnabled': + return false; + default: + return; + } + }), + kbnConfig: { get: jest.fn() }, + }; + const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + const mockDeps = ({ + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['superuser'], + username: 'Tom Riddle', + }), + }, + }, + router: httpSetup.createRouter(''), + licensing: { + license$: of({ + isActive: true, + isAvailable: true, + type: 'gold', + }), + }, + } as unknown) as any; + core = await createMockReportingCore(config, mockDeps); + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'printablePdf', + jobType: 'printable_pdf', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + core.getExportTypesRegistry = () => exportTypesRegistry; + }); -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -let mockServer: Hapi.Server; -let mockReportingPlugin: ReportingCore; -let mockReportingConfig: ReportingConfig; - -const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), -} as unknown) as Logger; - -beforeEach(async () => { - mockServer = new Hapi.Server({ - debug: false, - port: 8080, - routes: { log: { collect: true } }, + afterEach(async () => { + mockLogger.debug.mockReset(); + mockLogger.error.mockReset(); + await server.stop(); }); - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); -}); + it('returns 400 if there are no job params', async () => { + registerJobGenerationRoutes(core, mockLogger); -const mockPlugins = { - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -}; - -const getErrorsFromRequest = (request: Hapi.Request) => { - // @ts-ignore error property doesn't exist on RequestLog - return request.logs.filter((log) => log.tags.includes('error')).map((log) => log.error); // NOTE: error stack is available -}; - -test(`returns 400 if there are no job params`, async () => { - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - }; + await server.start(); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: A jobParams RISON string is required], - ] - `); -}); + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"A jobParams RISON string is required in the querystring or POST body"' + ) + ); + }); -test(`returns 400 if job params is invalid`, async () => { - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - payload: { jobParams: `foo:` }, - }; + it('returns 400 if job params query is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: invalid rison: foo:], - ] - `); -}); + await server.start(); -test(`returns 500 if job handler throws an error`, async () => { - mockReportingPlugin.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ - toJSON: () => { - throw new Error('you found me'); - }, - })); - - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - payload: { jobParams: `abc` }, - }; + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf?jobParams=foo:') + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 if job params body is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `foo:` }) + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 export type is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: you found me], - [Error: you found me], - ] - `); + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/TonyHawksProSkater2') + .send({ jobParams: `abc` }) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"') + ); + }); + + it('returns 400 if job handler throws an error', async () => { + const errorText = 'you found me'; + core.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ + toJSON: () => { + throw new Error(errorText); + }, + })); + + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `abc` }) + .expect(400) + .then(({ body }) => { + expect(body.message).toMatchInlineSnapshot(`"${errorText}"`); + }); + }); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 56faa37d5fcbd..f2e616c0803a7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; +import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; -import { Legacy } from 'kibana'; +import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { ReportingResponseToolkit } from './types'; +import { HandlerFunction } from './types'; const esErrors = elasticsearchErrors as Record; -export function registerJobGenerationRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { const config = reporting.getConfig(); const downloadBaseUrl = config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; @@ -32,48 +25,71 @@ export function registerJobGenerationRoutes( /* * Generates enqueued job details to use in responses */ - async function handler( - exportTypeId: string, - jobParams: object, - legacyRequest: Legacy.Request, - h: ReportingResponseToolkit - ) { - const request = makeRequestFacade(legacyRequest); - const user = request.pre.user; - const headers = request.headers; + const handler: HandlerFunction = async (user, exportTypeId, jobParams, context, req, res) => { + const licenseInfo = await reporting.getLicenseInfo(); + const licenseResults = licenseInfo[exportTypeId]; + + if (!licenseResults) { + return res.badRequest({ body: `Invalid export-type of ${exportTypeId}` }); + } + + if (!licenseResults.enableLinks) { + return res.forbidden({ body: licenseResults.message }); + } const enqueueJob = await reporting.getEnqueueJob(); - const job = await enqueueJob(exportTypeId, jobParams, user, headers, request); + const job = await enqueueJob(exportTypeId, jobParams, user, context, req); // return the queue's job information const jobJson = job.toJSON(); - return h - .response({ + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: { path: `${downloadBaseUrl}/${jobJson.id}`, job: jobJson, - }) - .type('application/json'); - } + }, + }); + }; + + function handleError(res: typeof kibanaResponseFactory, err: Error | Boom) { + if (err instanceof Boom) { + return res.customError({ + statusCode: err.output.statusCode, + body: err.output.payload.message, + }); + } - function handleError(exportTypeId: string, err: Error) { if (err instanceof esErrors['401']) { - return boom.unauthorized(`Sorry, you aren't authenticated`); + return res.unauthorized({ + body: `Sorry, you aren't authenticated`, + }); } + if (err instanceof esErrors['403']) { - return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); + return res.forbidden({ + body: `Sorry, you are not authorized`, + }); } + if (err instanceof esErrors['404']) { - return boom.boomify(err, { statusCode: 404 }); + return res.notFound({ + body: err.message, + }); } - return err; + + return res.badRequest({ + body: err.message, + }); } - registerGenerateFromJobParams(reporting, server, plugins, handler, handleError, logger); + registerGenerateFromJobParams(reporting, handler, handleError); // Register beta panel-action download-related API's if (config.get('csv', 'enablePanelActionDownload')) { - registerGenerateCsvFromSavedObject(reporting, server, plugins, handler, handleError, logger); - registerGenerateCsvFromSavedObjectImmediate(reporting, server, plugins, logger); + registerGenerateCsvFromSavedObject(reporting, handler, handleError); + registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger); } } diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/legacy/plugins/reporting/server/routes/index.ts index 556f4e12b077e..005d82086665c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/index.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/index.ts @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../'; import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; import { registerJobGenerationRoutes } from './generation'; import { registerJobInfoRoutes } from './jobs'; +import { ReportingCore } from '../core'; -export function registerRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { - registerJobGenerationRoutes(reporting, server, plugins, logger); - registerJobInfoRoutes(reporting, server, plugins, logger); +export function registerRoutes(reporting: ReportingCore, logger: Logger) { + registerJobGenerationRoutes(reporting, logger); + registerJobInfoRoutes(reporting); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts index 4f597bcee858e..73f3c660141c1 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts @@ -4,327 +4,347 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ReportingConfig, ReportingCore } from '../'; -import { LevelLogger } from '../lib'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; +import { registerJobInfoRoutes } from './jobs'; import { createMockReportingCore } from '../../test_helpers'; +import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { ExportTypeDefinition, ReportingSetupDeps } from '../types'; -import { registerJobInfoRoutes } from './jobs'; - -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -let mockServer: any; -let exportTypesRegistry: ExportTypesRegistry; -let mockReportingPlugin: ReportingCore; -let mockReportingConfig: ReportingConfig; -const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), -} as unknown) as LevelLogger; - -beforeEach(async () => { - mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); - exportTypesRegistry = new ExportTypesRegistry(); - exportTypesRegistry.register({ - id: 'unencoded', - jobType: 'unencodedJobType', - jobContentExtension: 'csv', - } as ExportTypeDefinition); - exportTypesRegistry.register({ - id: 'base64Encoded', - jobType: 'base64EncodedJobType', - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - } as ExportTypeDefinition); - - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getExportTypesRegistry = () => exportTypesRegistry; -}); - -const mockPlugins = ({ - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -} as unknown) as ReportingSetupDeps; - -const getHits = (...sources: any) => { - return { - hits: { - hits: sources.map((source: object) => ({ _source: source })), - }, - }; -}; - -const getErrorsFromRequest = (request: any) => - request.logs.filter((log: any) => log.tags.includes('error')).map((log: any) => log.error); - -test(`returns 404 if job not found`, async () => { - // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - const response = await mockServer.inject(request); - const { statusCode } = response; - expect(statusCode).toBe(404); -}); - -test(`returns 401 if not valid job type`, async () => { - // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', +import { ExportTypeDefinition } from '../types'; +import { LevelLogger } from '../lib'; +import { ReportingInternalSetup } from '../core'; +import { of } from 'rxjs'; + +type setupServerReturn = UnwrapPromise>; + +describe('GET /api/reporting/jobs/download', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + + const config = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; + const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + const getHits = (...sources: any) => { + return { + hits: { + hits: sources.map((source: object) => ({ _source: source })), + }, + }; }; - const { statusCode } = await mockServer.inject(request); - expect(statusCode).toBe(401); -}); - -describe(`when job is incomplete`, () => { - const getIncompleteResponse = async () => { + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + core = await createMockReportingCore(config, ({ + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['superuser'], + username: 'Tom Riddle', + }), + }, + }, + router: httpSetup.createRouter(''), + licensing: { + license$: of({ + isActive: true, + isAvailable: true, + type: 'gold', + }), + }, + } as unknown) as ReportingInternalSetup); // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ), - }; + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'unencoded', + jobType: 'unencodedJobType', + jobContentExtension: 'csv', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + exportTypesRegistry.register({ + id: 'base64Encoded', + jobType: 'base64EncodedJobType', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + core.getExportTypesRegistry = () => exportTypesRegistry; + }); - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + afterEach(async () => { + mockLogger.debug.mockReset(); + mockLogger.error.mockReset(); + await server.stop(); + }); - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', + it('fails on malformed download IDs', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; + registerJobInfoRoutes(core); - return await mockServer.inject(request); - }; + await server.start(); - test(`sets statusCode to 503`, async () => { - const { statusCode } = await getIncompleteResponse(); - expect(statusCode).toBe(503); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/1') + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"[request params.docId]: value has length [1] but it must have a minimum length of [3]."' + ) + ); }); - test(`uses status as payload`, async () => { - const { payload } = await getIncompleteResponse(); - expect(payload).toBe('pending'); - }); + it('fails on unauthenticated users', async () => { + // @ts-ignore + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => undefined, + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); + await server.start(); - test(`sets retry-after header to 30`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['retry-after']).toBe(30); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(401) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`) + ); }); -}); -describe(`when job is failed`, () => { - const getFailedResponse = async () => { - const hits = getHits({ - jobtype: 'unencodedJobType', - status: 'failed', - output: { content: 'job failure message' }, - }); + it('fails on users without the appropriate role', async () => { // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; - - test(`sets status code to 500`, async () => { - const { statusCode } = await getFailedResponse(); - expect(statusCode).toBe(500); - }); + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['peasant'], + username: 'Tom Riddle', + }), + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getFailedResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); + await server.start(); - test(`sets the payload.reason to the job content`, async () => { - const { payload } = await getFailedResponse(); - expect(JSON.parse(payload).reason).toBe('job failure message'); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(403) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you don't have access to Reporting"`) + ); }); -}); -describe(`when job is completed`, () => { - const getCompletedResponse = async ({ - jobType = 'unencodedJobType', - outputContent = 'job output content', - outputContentType = 'application/pdf', - title = '', - } = {}) => { - const hits = getHits({ - jobtype: jobType, - status: 'completed', - output: { content: outputContent, content_type: outputContentType }, - payload: { - title, - }, - }); + it('returns 404 if job not found', async () => { // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + registerJobInfoRoutes(core); - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; + await server.start(); - test(`sets statusCode to 200`, async () => { - const { statusCode, request } = await getCompletedResponse(); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(statusCode).toBe(200); + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(404); }); - test(`doesn't encode output content for not-specified jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'unencodedJobType', - outputContent: 'test', - }); + it('returns a 401 if not a valid job type', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; + registerJobInfoRoutes(core); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + await server.start(); - expect(payload).toBe('test'); + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(401); }); - test(`base64 encodes output content for configured jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'base64EncodedJobType', - outputContent: 'test', - }); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + it('when a job is incomplete', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(503) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Retry-After', '30') + .then(({ text }) => expect(text).toEqual('pending')); + }); - expect(payload).toBe(Buffer.from('test', 'base64').toString()); + it('when a job fails', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue( + Promise.resolve( + getHits({ + jobtype: 'unencodedJobType', + status: 'failed', + output: { content: 'job failure message' }, + }) + ) + ), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(500) + .expect('Content-Type', 'application/json; charset=utf-8') + .then(({ body }) => + expect(body.message).toEqual('Reporting generation failed: job failure message') + ); }); - test(`specifies text/csv; charset=utf-8 contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ outputContentType: 'text/csv' }); + describe('successful downloads', () => { + const getCompleteHits = async ({ + jobType = 'unencodedJobType', + outputContent = 'job output content', + outputContentType = 'text/plain', + title = '', + } = {}) => { + return getHits({ + jobtype: jobType, + status: 'completed', + output: { content: outputContent, content_type: outputContentType }, + payload: { + title, + }, + }); + }; - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + it('when a known job-type is complete', async () => { + const hits = getCompleteHits(); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('content-disposition', 'inline; filename="report.csv"'); + }); - expect(headers['content-type']).toBe('text/csv; charset=utf-8'); - }); + it('succeeds when security is not there or disabled', async () => { + const hits = getCompleteHits(); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; - test(`specifies default filename in content-disposition header if no title`, async () => { - const { headers, request } = await getCompletedResponse({}); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.csv"'); - }); + // @ts-ignore + core.pluginSetupDeps.security = null; - test(`specifies payload title in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ title: 'something' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="something.csv"'); - }); + registerJobInfoRoutes(core); - test(`specifies jobContentExtension in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ jobType: 'base64EncodedJobType' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.pdf"'); - }); + await server.start(); - test(`specifies application/pdf contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ - outputContentType: 'application/pdf', + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('content-disposition', 'inline; filename="report.csv"'); }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-type']).toBe('application/pdf'); - }); - describe(`when non-whitelisted contentType specified in job output`, () => { - test(`sets statusCode to 500`, async () => { - const { statusCode, request } = await getCompletedResponse({ - outputContentType: 'application/html', + it(`doesn't encode output-content for non-specified job-types`, async () => { + const hits = getCompleteHits({ + jobType: 'unencodedJobType', + outputContent: 'test', }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); - expect(statusCode).toBe(500); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .then(({ text }) => expect(text).toEqual('test')); }); - test(`doesn't include job output content in payload`, async () => { - const { payload, request } = await getCompletedResponse({ - outputContentType: 'application/html', + it(`base64 encodes output content for configured jobTypes`, async () => { + const hits = getCompleteHits({ + jobType: 'base64EncodedJobType', + outputContent: 'test', + outputContentType: 'application/pdf', }); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'application/pdf') + .expect('content-disposition', 'inline; filename="report.pdf"') + .then(({ body }) => expect(Buffer.from(body).toString('base64')).toEqual('test')); }); - test(`logs error message about invalid content type`, async () => { - const { request } = await getCompletedResponse({ outputContentType: 'application/html' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); + it('refuses to return unknown content-types', async () => { + const hits = getCompleteHits({ + jobType: 'unencodedJobType', + outputContent: 'alert("all your base mine now");', + outputContentType: 'application/html', + }); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(400) + .then(({ body }) => { + expect(body).toEqual({ + error: 'Bad Request', + message: 'Unsupported content-type of application/html specified by job output', + statusCode: 400, + }); + }); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index 59090961998af..8c35f79ec0fb4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -5,24 +5,15 @@ */ import Boom from 'boom'; -import { ResponseObject } from 'hapi'; -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; import { jobsQueryFactory } from '../lib/jobs_query'; -import { JobDocOutput, JobSource, ReportingSetupDeps, ServerFacade } from '../types'; import { deleteJobResponseHandlerFactory, downloadJobResponseHandlerFactory, } from './lib/job_response_handler'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { - getRouteConfigFactoryDeletePre, - getRouteConfigFactoryDownloadPre, - getRouteConfigFactoryManagementPre, -} from './lib/route_config_factories'; -import { ReportingResponseToolkit } from './types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; interface ListQuery { page: string; @@ -31,193 +22,192 @@ interface ListQuery { } const MAIN_ENTRY = `${API_BASE_URL}/jobs`; -function isResponse(response: Boom | ResponseObject): response is ResponseObject { - return !(response as Boom).isBoom; -} - -export function registerJobInfoRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { +export async function registerJobInfoRoutes(reporting: ReportingCore) { const config = reporting.getConfig(); - const { elasticsearch } = plugins; + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { elasticsearch, router } = setupDeps; const jobsQuery = jobsQueryFactory(config, elasticsearch); - const getRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); // list jobs in the queue, paginated - server.route({ - path: `${MAIN_ENTRY}/list`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { page: queryPage, size: querySize, ids: queryIds } = request.query as ListQuery; + router.get( + { + path: `${MAIN_ENTRY}/list`, + validate: false, + }, + userHandler(async (user, context, req, res) => { + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + const { + page: queryPage = '0', + size: querySize = '10', + ids: queryIds = null, + } = req.query as ListQuery; const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; + const results = await jobsQuery.list(jobTypes, user, page, size, jobIds); - const results = jobsQuery.list( - request.pre.management.jobTypes, - request.pre.user, - page, - size, - jobIds - ); - return results; - }, - }); + return res.ok({ + body: results, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); // return the count of all jobs in the queue - server.route({ - path: `${MAIN_ENTRY}/count`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const results = jobsQuery.count(request.pre.management.jobTypes, request.pre.user); - return results; + router.get( + { + path: `${MAIN_ENTRY}/count`, + validate: false, }, - }); + userHandler(async (user, context, req, res) => { + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const count = await jobsQuery.count(jobTypes, user); + + return res.ok({ + body: count.toString(), + headers: { + 'content-type': 'text/plain', + }, + }); + }) + ); // return the raw output from a job - server.route({ - path: `${MAIN_ENTRY}/output/{docId}`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - return jobsQuery.get(request.pre.user, docId, { includeContent: true }).then( - (result): JobDocOutput => { - if (!result) { - throw Boom.notFound(); - } - const { - _source: { jobtype: jobType, output: jobOutput }, - } = result; - - if (!request.pre.management.jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } - - return jobOutput; - } - ); + router.get( + { + path: `${MAIN_ENTRY}/output/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 2 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const result = await jobsQuery.get(user, docId, { includeContent: true }); + + if (!result) { + throw Boom.notFound(); + } + + const { + _source: { jobtype: jobType, output: jobOutput }, + } = result; + + if (!jobTypes.includes(jobType)) { + throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); + } + + return res.ok({ + body: jobOutput, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); // return some info about the job - server.route({ - path: `${MAIN_ENTRY}/info/{docId}`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - return jobsQuery.get(request.pre.user, docId).then((result): JobSource['_source'] => { - if (!result) { - throw Boom.notFound(); - } - - const { _source: job } = result; - const { jobtype: jobType, payload: jobPayload } = job; - if (!request.pre.management.jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); - } - - return { + router.get( + { + path: `${MAIN_ENTRY}/info/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 2 }), + }), + }, + }, + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const result = await jobsQuery.get(user, docId); + + if (!result) { + throw Boom.notFound(); + } + + const { _source: job } = result; + const { jobtype: jobType, payload: jobPayload } = job; + + if (!jobTypes.includes(jobType)) { + throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); + } + + return res.ok({ + body: { ...job, payload: { ...jobPayload, headers: undefined, }, - }; + }, + headers: { + 'content-type': 'application/json', + }, }); - }, - }); + }) + ); // trigger a download of the output from a job const exportTypesRegistry = reporting.getExportTypesRegistry(); - const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(config, plugins, logger); - const downloadResponseHandler = downloadJobResponseHandlerFactory(config, elasticsearch, exportTypesRegistry); // prettier-ignore - server.route({ - path: `${MAIN_ENTRY}/download/{docId}`, - method: 'GET', - options: getRouteConfigDownload(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - let response = await downloadResponseHandler( - request.pre.management.jobTypes, - request.pre.user, - h, - { docId } - ); - - if (isResponse(response)) { - const { statusCode } = response; - - if (statusCode !== 200) { - if (statusCode === 500) { - logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); - } else { - logger.debug( - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); - } - } - - response = response.header('accept-ranges', 'none'); - } - - return response; + const downloadResponseHandler = downloadJobResponseHandlerFactory( + config, + elasticsearch, + exportTypesRegistry + ); + + router.get( + { + path: `${MAIN_ENTRY}/download/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + return downloadResponseHandler(res, jobTypes, user, { docId }); + }) + ); // allow a report to be deleted - const getRouteConfigDelete = getRouteConfigFactoryDeletePre(config, plugins, logger); const deleteResponseHandler = deleteJobResponseHandlerFactory(config, elasticsearch); - server.route({ - path: `${MAIN_ENTRY}/delete/{docId}`, - method: 'DELETE', - options: getRouteConfigDelete(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - let response = await deleteResponseHandler( - request.pre.management.jobTypes, - request.pre.user, - h, - { docId } - ); - - if (isResponse(response)) { - const { statusCode } = response; - - if (statusCode !== 200) { - if (statusCode === 500) { - logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); - } else { - logger.debug( - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); - } - } - - response = response.header('accept-ranges', 'none'); - } - - return response; + router.delete( + { + path: `${MAIN_ENTRY}/delete/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + return deleteResponseHandler(res, jobTypes, user, { docId }); + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js deleted file mode 100644 index 2c80965432cd2..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js +++ /dev/null @@ -1,175 +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 { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; - -describe('authorized_user_pre_routing', function () { - const createMockConfig = (mockConfig = {}) => { - return { - get: (...keys) => mockConfig[keys.join('.')], - kbnConfig: { get: (...keys) => mockConfig[keys.join('.')] }, - }; - }; - const createMockPlugins = (function () { - const getUserStub = jest.fn(); - - return function ({ - securityEnabled = true, - xpackInfoUndefined = false, - xpackInfoAvailable = true, - getCurrentUser = undefined, - user = undefined, - }) { - getUserStub.mockReset(); - getUserStub.mockResolvedValue(user); - return { - security: securityEnabled - ? { - authc: { getCurrentUser }, - } - : null, - __LEGACY: { - plugins: { - xpack_main: { - info: !xpackInfoUndefined && { - isAvailable: () => xpackInfoAvailable, - feature(featureName) { - if (featureName === 'security') { - return { - isEnabled: () => securityEnabled, - isAvailable: () => xpackInfoAvailable, - }; - } - }, - }, - }, - }, - }, - }; - }; - })(); - - const mockRequestRaw = { - body: {}, - events: {}, - headers: {}, - isSystemRequest: false, - params: {}, - query: {}, - route: { settings: { payload: 'abc' }, options: { authRequired: true, body: {}, tags: [] } }, - withoutSecretHeaders: true, - }; - const getMockRequest = () => ({ - ...mockRequestRaw, - raw: { req: mockRequestRaw }, - }); - - const getMockLogger = () => ({ - warn: jest.fn(), - error: (msg) => { - throw new Error(msg); - }, - }); - - it('should return with boom notFound when xpackInfo is undefined', async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ xpackInfoUndefined: true }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(404); - }); - - it(`should return with boom notFound when xpackInfo isn't available`, async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ xpackInfoAvailable: false }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(404); - }); - - it('should return with null user when security is disabled in Elasticsearch', async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ securityEnabled: false }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toBe(null); - }); - - it('should return with boom unauthenticated when security is enabled but no authenticated user', async function () { - const mockPlugins = createMockPlugins({ - user: null, - config: { 'xpack.reporting.roles.allow': ['.reporting_user'] }, - }); - mockPlugins.security = { authc: { getCurrentUser: () => null } }; - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(401); - }); - - it(`should return with boom forbidden when security is enabled but user doesn't have allowed role`, async function () { - const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); - const mockPlugins = createMockPlugins({ - user: { roles: [] }, - getCurrentUser: () => ({ roles: ['something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(403); - }); - - it('should return with user when security is enabled and user has explicitly allowed role', async function () { - const user = { roles: ['.reporting_user', 'something_else'] }; - const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); - const mockPlugins = createMockPlugins({ - user, - getCurrentUser: () => ({ roles: ['.reporting_user', 'something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toEqual(user); - }); - - it('should return with user when security is enabled and user has superuser role', async function () { - const user = { roles: ['superuser', 'something_else'] }; - const mockConfig = createMockConfig({ 'roles.allow': [] }); - const mockPlugins = createMockPlugins({ - getCurrentUser: () => ({ roles: ['superuser', 'something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toEqual(user); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts new file mode 100644 index 0000000000000..4cb7af3d0d409 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; +import sinon from 'sinon'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { ReportingConfig, ReportingCore } from '../../'; +import { createMockReportingCore } from '../../../test_helpers'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; +import { ReportingInternalSetup } from '../../core'; + +let mockConfig: ReportingConfig; +let mockCore: ReportingCore; + +const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, +}); + +const getMockContext = () => + (({ + core: coreMock.createRequestHandlerContext(), + } as unknown) as RequestHandlerContext); + +const getMockRequest = () => + ({ + url: { port: '5601', query: '', path: '/foo' }, + route: { path: '/foo', options: {} }, + } as KibanaRequest); + +const getMockResponseFactory = () => + (({ + ...httpServerMock.createResponseFactory(), + forbidden: (obj: unknown) => obj, + unauthorized: (obj: unknown) => obj, + } as unknown) as KibanaResponseFactory); + +beforeEach(async () => { + const mockConfigGet = sinon.stub().withArgs('roles', 'allow').returns(['reporting_user']); + mockConfig = getMockConfig(mockConfigGet); + mockCore = await createMockReportingCore(mockConfig); +}); + +describe('authorized_user_pre_routing', function () { + it('should return from handler with null user when security is disabled', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: undefined, // disable security + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; + + let handlerCalled = false; + authorizedUserPreRouting((user: unknown) => { + expect(user).toBe(null); // verify the user is a null value + handlerCalled = true; + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + + expect(handlerCalled).toBe(true); + }); + + it('should return with 401 when security is enabled but no authenticated user', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { getCurrentUser: () => null }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + const requestHandler = authorizedUserPreRouting(mockHandler); + const mockResponseFactory = getMockResponseFactory(); + + expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({ + body: `Sorry, you aren't authenticated`, + }); + }); + + it(`should return with 403 when security is enabled but user doesn't have allowed role`, async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + expect( + authorizedUserPreRouting(mockHandler)(getMockContext(), getMockRequest(), mockResponseFactory) + ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); + }); + + it('should return from handler when security is enabled and user has explicitly allowed role', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => ({ username: 'friendlyuser', roles: ['reporting_user'] }), + }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + let handlerCalled = false; + authorizedUserPreRouting((user: unknown) => { + expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); + handlerCalled = true; + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + + expect(handlerCalled).toBe(true); + }); + + it('should return from handler when security is enabled and user has superuser role', async function () {}); +}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 0c4e75a53831e..87582ca3ca239 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -4,52 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { Legacy } from 'kibana'; +import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/server'; -import { ReportingConfig } from '../../../server'; -import { LevelLogger as Logger } from '../../../server/lib'; -import { ReportingSetupDeps } from '../../../server/types'; import { getUserFactory } from '../../lib/get_user'; +import { ReportingCore } from '../../core'; +type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; -export type PreRoutingFunction = ( - request: Legacy.Request -) => Promise | AuthenticatedUser | null>; +export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R + ? (user: ReportingUser, ...a: U) => R + : never; export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger + reporting: ReportingCore ) { - const getUser = getUserFactory(plugins.security, logger); - const { info: xpackInfo } = plugins.__LEGACY.plugins.xpack_main; - - return async function authorizedUserPreRouting(request: Legacy.Request) { - if (!xpackInfo || !xpackInfo.isAvailable()) { - logger.warn('Unable to authorize user before xpack info is available.', [ - 'authorizedUserPreRouting', - ]); - return Boom.notFound(); - } - - const security = xpackInfo.feature('security'); - if (!security.isEnabled() || !security.isAvailable()) { - return null; - } - - const user = await getUser(request); - - if (!user) { - return Boom.unauthorized(`Sorry, you aren't authenticated`); - } - - const authorizedRoles = [superuserRole, ...(config.get('roles', 'allow') as string[])]; - if (!user.roles.find((role) => authorizedRoles.includes(role))) { - return Boom.forbidden(`Sorry, you don't have access to Reporting`); - } - - return user; + const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); + const getUser = getUserFactory(setupDeps.security); + return (handler: RequestHandlerUser): RequestHandler => { + return (context, req, res) => { + let user: ReportingUser = null; + if (setupDeps.security) { + // find the authenticated user, or null if security is not enabled + user = getUser(req); + if (!user) { + // security is enabled but the user is null + return res.unauthorized({ body: `Sorry, you aren't authenticated` }); + } + } + + if (user) { + // check allowance with the configured set of roleas + "superuser" + const allowedRoles = config.get('roles', 'allow') || []; + const authorizedRoles = [superuserRole, ...allowedRoles]; + + if (!user.roles.find((role) => authorizedRoles.includes(role))) { + // user's roles do not allow + return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); + } + } + + return handler(user, context, req, res); + }; }; }; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index 6a228c1915615..e16f5278c8cc7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -12,15 +12,10 @@ import { statuses } from '../../lib/esqueue/constants/statuses'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; import { ExportTypeDefinition, JobDocOutput, JobSource } from '../../types'; -interface ICustomHeaders { - [x: string]: any; -} - type ExportTypeType = ExportTypeDefinition; interface ErrorFromPayload { message: string; - reason: string | null; } // A camelCase version of JobDocOutput @@ -37,7 +32,7 @@ const getTitle = (exportType: ExportTypeType, title?: string): string => `${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`; const getReportingHeaders = (output: JobDocOutput, exportType: ExportTypeType) => { - const metaDataHeaders: ICustomHeaders = {}; + const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE) { const csvContainsFormulas = _.get(output, 'csv_contains_formulas', false); @@ -76,12 +71,13 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist }; } + // @TODO: These should be semantic HTTP codes as 500/503's indicate + // error then these are really operating properly. function getFailure(output: JobDocOutput): Payload { return { statusCode: 500, content: { - message: 'Reporting generation failed', - reason: output.content, + message: `Reporting generation failed: ${output.content}`, }, contentType: 'application/json', headers: {}, @@ -92,7 +88,7 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist return { statusCode: 503, content: status, - contentType: 'application/json', + contentType: 'text/plain', headers: { 'retry-after': 30 }, }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 174ec15c81d8a..990af2d0aca80 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { ResponseToolkit } from 'hapi'; -import { ElasticsearchServiceSetup } from 'kibana/server'; +import { ElasticsearchServiceSetup, kibanaResponseFactory } from 'kibana/server'; +import { AuthenticatedUser } from '../../../../../../plugins/security/server'; import { ReportingConfig } from '../../'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; @@ -29,40 +28,43 @@ export function downloadJobResponseHandlerFactory( const jobsQuery = jobsQueryFactory(config, elasticsearch); const getDocumentPayload = getDocumentPayloadFactory(exportTypesRegistry); - return function jobResponseHandler( + return async function jobResponseHandler( + res: typeof kibanaResponseFactory, validJobTypes: string[], - user: any, - h: ResponseToolkit, + user: AuthenticatedUser | null, params: JobResponseHandlerParams, opts: JobResponseHandlerOpts = {} ) { const { docId } = params; - // TODO: async/await - return jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }).then((doc) => { - if (!doc) return Boom.notFound(); - const { jobtype: jobType } = doc._source; - if (!validJobTypes.includes(jobType)) { - return Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } + const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }); + if (!doc) { + return res.notFound(); + } - const output = getDocumentPayload(doc); + const { jobtype: jobType } = doc._source; - if (!WHITELISTED_JOB_CONTENT_TYPES.includes(output.contentType)) { - return Boom.badImplementation( - `Unsupported content-type of ${output.contentType} specified by job output` - ); - } + if (!validJobTypes.includes(jobType)) { + return res.unauthorized({ + body: `Sorry, you are not authorized to download ${jobType} reports`, + }); + } - const response = h.response(output.content).type(output.contentType).code(output.statusCode); + const response = getDocumentPayload(doc); - if (output.headers) { - Object.keys(output.headers).forEach((key) => { - response.header(key, output.headers[key]); - }); - } + if (!WHITELISTED_JOB_CONTENT_TYPES.includes(response.contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${response.contentType} specified by job output`, + }); + } - return response; // Hapi + return res.custom({ + body: typeof response.content === 'string' ? Buffer.from(response.content) : response.content, + statusCode: response.statusCode, + headers: { + ...response.headers, + 'content-type': response.contentType, + }, }); }; } @@ -74,26 +76,37 @@ export function deleteJobResponseHandlerFactory( const jobsQuery = jobsQueryFactory(config, elasticsearch); return async function deleteJobResponseHander( + res: typeof kibanaResponseFactory, validJobTypes: string[], - user: any, - h: ResponseToolkit, + user: AuthenticatedUser | null, params: JobResponseHandlerParams ) { const { docId } = params; const doc = await jobsQuery.get(user, docId, { includeContent: false }); - if (!doc) return Boom.notFound(); + + if (!doc) { + return res.notFound(); + } const { jobtype: jobType } = doc._source; + if (!validJobTypes.includes(jobType)) { - return Boom.unauthorized(`Sorry, you are not authorized to delete ${jobType} reports`); + return res.unauthorized({ + body: `Sorry, you are not authorized to delete ${jobType} reports`, + }); } try { const docIndex = doc._index; await jobsQuery.delete(docIndex, docId); - return h.response({ deleted: true }); + return res.ok({ + body: { deleted: true }, + }); } catch (error) { - return Boom.boomify(error, { statusCode: error.statusCode }); + return res.customError({ + statusCode: error.statusCode, + body: error.message, + }); } }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts deleted file mode 100644 index 8cdb7b4c018d7..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts +++ /dev/null @@ -1,62 +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 { Legacy } from 'kibana'; -import { makeRequestFacade } from './make_request_facade'; - -describe('makeRequestFacade', () => { - test('creates a default object', () => { - const legacyRequest = ({ - getBasePath: () => 'basebase', - params: { - param1: 123, - }, - payload: { - payload1: 123, - }, - headers: { - user: 123, - }, - } as unknown) as Legacy.Request; - - expect(makeRequestFacade(legacyRequest)).toMatchInlineSnapshot(` - Object { - "getBasePath": [Function], - "getRawRequest": [Function], - "getSavedObjectsClient": undefined, - "headers": Object { - "user": 123, - }, - "params": Object { - "param1": 123, - }, - "payload": Object { - "payload1": 123, - }, - "pre": undefined, - "query": undefined, - "route": undefined, - } - `); - }); - - test('getRawRequest', () => { - const legacyRequest = ({ - getBasePath: () => 'basebase', - params: { - param1: 123, - }, - payload: { - payload1: 123, - }, - headers: { - user: 123, - }, - } as unknown) as Legacy.Request; - - expect(makeRequestFacade(legacyRequest).getRawRequest()).toBe(legacyRequest); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts deleted file mode 100644 index 5dd62711f2565..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts +++ /dev/null @@ -1,32 +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 { RequestQuery } from 'hapi'; -import { Legacy } from 'kibana'; -import { - RequestFacade, - ReportingRequestPayload, - ReportingRequestPre, - ReportingRequestQuery, -} from '../../../server/types'; - -export function makeRequestFacade(request: Legacy.Request): RequestFacade { - // This condition is for unit tests - const getSavedObjectsClient = request.getSavedObjectsClient - ? request.getSavedObjectsClient.bind(request) - : request.getSavedObjectsClient; - return { - getSavedObjectsClient, - headers: request.headers, - params: request.params, - payload: (request.payload as object) as ReportingRequestPayload, - query: ((request.query as RequestQuery) as object) as ReportingRequestQuery, - pre: (request.pre as Record) as ReportingRequestPre, - getBasePath: request.getBasePath, - route: request.route, - getRawRequest: () => request, - }; -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts deleted file mode 100644 index f9c7571e25bac..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts +++ /dev/null @@ -1,36 +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 { Legacy } from 'kibana'; -import { ReportingConfig } from '../../'; -import { LevelLogger as Logger } from '../../lib'; -import { ReportingSetupDeps } from '../../types'; - -export type GetReportingFeatureIdFn = (request: Legacy.Request) => string; - -export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -) { - const xpackMainPlugin = plugins.__LEGACY.plugins.xpack_main; - const pluginId = 'reporting'; - - // License checking and enable/disable logic - return function reportingFeaturePreRouting(getReportingFeatureId: GetReportingFeatureIdFn) { - return function licensePreRouting(request: Legacy.Request) { - const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); - const reportingFeatureId = getReportingFeatureId(request) as string; - const reportingFeature = licenseCheckResults[reportingFeatureId]; - if (!reportingFeature.showLinks || !reportingFeature.enableLinks) { - throw Boom.forbidden(reportingFeature.message); - } else { - return reportingFeature; - } - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts deleted file mode 100644 index 0ee9db4678684..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts +++ /dev/null @@ -1,130 +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 Joi from 'joi'; -import { ReportingConfig } from '../../'; -import { LevelLogger as Logger } from '../../lib'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { ReportingSetupDeps } from '../../types'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; -import { - GetReportingFeatureIdFn, - reportingFeaturePreRoutingFactory, -} from './reporting_feature_pre_routing'; - -const API_TAG = 'api'; - -export interface RouteConfigFactory { - tags?: string[]; - pre: any[]; - response?: { - ranges: boolean; - }; -} - -export type GetRouteConfigFactoryFn = ( - getFeatureId?: GetReportingFeatureIdFn -) => RouteConfigFactory; - -export function getRouteConfigFactoryReportingPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); - - return (getFeatureId?: GetReportingFeatureIdFn): RouteConfigFactory => { - const preRouting: any[] = [{ method: authorizedUserPreRouting, assign: 'user' }]; - if (getFeatureId) { - preRouting.push(reportingFeaturePreRouting(getFeatureId)); - } - - return { - tags: [API_TAG], - pre: preRouting, - }; - }; -} - -export function getRouteOptionsCsv( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -) { - const getRouteConfig = getRouteConfigFactoryReportingPre(config, plugins, logger); - return { - ...getRouteConfig(() => CSV_FROM_SAVEDOBJECT_JOB_TYPE), - validate: { - params: Joi.object({ - savedObjectType: Joi.string().required(), - savedObjectId: Joi.string().required(), - }).required(), - payload: Joi.object({ - state: Joi.object().default({}), - timerange: Joi.object({ - timezone: Joi.string().default('UTC'), - min: Joi.date().required(), - max: Joi.date().required(), - }).optional(), - }), - }, - }; -} - -export function getRouteConfigFactoryManagementPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); - const managementPreRouting = reportingFeaturePreRouting(() => 'management'); - - return (): RouteConfigFactory => { - return { - pre: [ - { method: authorizedUserPreRouting, assign: 'user' }, - { method: managementPreRouting, assign: 'management' }, - ], - }; - }; -} - -// NOTE: We're disabling range request for downloading the PDF. There's a bug in Firefox's PDF.js viewer -// (https://github.com/mozilla/pdf.js/issues/8958) where they're using a range request to retrieve the -// TOC at the end of the PDF, but it's sending multiple cookies and causing our auth to fail with a 401. -// Additionally, the range-request doesn't alleviate any performance issues on the server as the entire -// download is loaded into memory. -export function getRouteConfigFactoryDownloadPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); - return (): RouteConfigFactory => ({ - ...getManagementRouteConfig(), - tags: [API_TAG, 'download'], - response: { - ranges: false, - }, - }); -} - -export function getRouteConfigFactoryDeletePre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); - return (): RouteConfigFactory => ({ - ...getManagementRouteConfig(), - tags: [API_TAG, 'delete'], - response: { - ranges: false, - }, - }); -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts index 2ebe1ada418dc..afa3fd3358fc1 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { KibanaResponseFactory, KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../../../plugins/security/common/model/authenticated_user'; import { JobDocPayload } from '../types'; export type HandlerFunction = ( + user: AuthenticatedUser | null, exportType: string, jobParams: object, - request: Legacy.Request, - h: ReportingResponseToolkit + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory ) => any; -export type HandlerErrorFunction = (exportType: string, err: Error) => any; +export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; export interface QueuedJobPayload { error?: boolean; @@ -24,5 +27,3 @@ export interface QueuedJobPayload { }; }; } - -export type ReportingResponseToolkit = Legacy.ResponseToolkit; diff --git a/x-pack/legacy/plugins/reporting/server/types.ts b/x-pack/legacy/plugins/reporting/server/types.ts index bfab568fe9fb3..2ccc209c3ce50 100644 --- a/x-pack/legacy/plugins/reporting/server/types.ts +++ b/x-pack/legacy/plugins/reporting/server/types.ts @@ -5,6 +5,7 @@ */ import { Legacy } from 'kibana'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -53,8 +54,8 @@ export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPa export interface TimeRangeParams { timezone: string; - min: Date | string | number; - max: Date | string | number; + min: Date | string | number | null; + max: Date | string | number | null; } export interface JobParamPostPayload { @@ -189,22 +190,10 @@ export interface LegacySetup { * Internal Types */ -export interface RequestFacade { - getBasePath: Legacy.Request['getBasePath']; - getSavedObjectsClient: Legacy.Request['getSavedObjectsClient']; - headers: Legacy.Request['headers']; - params: Legacy.Request['params']; - payload: JobParamPostPayload | GenerateExportTypePayload; - query: ReportingRequestQuery; - route: Legacy.Request['route']; - pre: ReportingRequestPre; - getRawRequest: () => Legacy.Request; -} - export type ESQueueCreateJobFn = ( jobParams: JobParamsType, - headers: Record, - request: RequestFacade + context: RequestHandlerContext, + request: KibanaRequest ) => Promise; export type ESQueueWorkerExecuteFn = ( diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts index 286e072f1f8f6..f6dbccdfe3980 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts @@ -12,17 +12,21 @@ jest.mock('../server/lib/create_queue'); jest.mock('../server/lib/enqueue_job'); jest.mock('../server/lib/validate'); +import { of } from 'rxjs'; import { EventEmitter } from 'events'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { coreMock } from 'src/core/server/mocks'; import { ReportingConfig, ReportingCore, ReportingPlugin } from '../server'; import { ReportingSetupDeps, ReportingStartDeps } from '../server/types'; +import { ReportingInternalSetup } from '../server/core'; const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => { return { elasticsearch: setupMock.elasticsearch, security: setupMock.security, - licensing: {} as any, + licensing: { + license$: of({ isAvailable: true, isActive: true, type: 'basic' }), + } as any, usageCollection: {} as any, __LEGACY: { plugins: { xpack_main: { status: new EventEmitter() } } } as any, }; @@ -49,8 +53,18 @@ const createMockReportingPlugin = async (config: ReportingConfig): Promise => { +export const createMockReportingCore = async ( + config: ReportingConfig, + setupDepsMock?: ReportingInternalSetup +): Promise => { config = config || {}; const plugin = await createMockReportingPlugin(config); - return plugin.getReportingCore(); + const core = plugin.getReportingCore(); + + if (setupDepsMock) { + // @ts-ignore overwriting private properties + core.pluginSetupDeps = setupDepsMock; + } + + return core; }; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts index 819636b714631..01b9f6cbd9cd6 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts @@ -4,9 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServerFacade } from '../server/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createHttpServer, createCoreContext } from 'src/core/server/http/test_utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ContextService } from 'src/core/server/context/context_service'; -export const createMockServer = (): ServerFacade => { - const mockServer = {}; - return mockServer as any; +const coreId = Symbol('reporting'); + +export const createMockServer = async () => { + const coreContext = createCoreContext({ coreId }); + const contextService = new ContextService(coreContext); + + const server = createHttpServer(coreContext); + const httpSetup = await server.setup({ + context: contextService.setup({ pluginDependencies: new Map() }), + }); + const handlerContext = coreMock.createRequestHandlerContext(); + + httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => { + return handlerContext; + }); + + return { + server, + httpSetup, + handlerContext, + }; }; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index d068711b87c9d..e44bd92c42391 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -13,7 +13,8 @@ "uiActions", "embeddable", "share", - "kibanaLegacy" + "kibanaLegacy", + "licensing" ], "server": true, "ui": true diff --git a/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts b/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts index 7d11403add136..90f97d44da224 100644 --- a/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts +++ b/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts @@ -46,7 +46,7 @@ export default function ({ getService }: FtrProviderContext) { jobParams: 0, })) as supertest.Response; - expect(resText).to.match(/\\\"jobParams\\\" must be a string/); + expect(resText).to.match(/expected value of type \[string\] but got \[number\]/); expect(resStatus).to.eql(400); }); From d0aeadf13eb8f7621d21f41eae480f011a561e64 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 30 May 2020 01:01:59 +0100 Subject: [PATCH 29/38] chore(NA): use env var to point config folder on os_packages built with fpm (#67433) Co-authored-by: Elastic Machine --- src/core/server/path/index.ts | 1 - .../tasks/os_packages/service_templates/sysv/etc/default/kibana | 2 ++ .../tasks/os_packages/service_templates/sysv/etc/init.d/kibana | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/server/path/index.ts b/src/core/server/path/index.ts index d482a32b32ae4..2e05e3856bd4c 100644 --- a/src/core/server/path/index.ts +++ b/src/core/server/path/index.ts @@ -28,7 +28,6 @@ const CONFIG_PATHS = [ process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), process.env.CONFIG_PATH, // deprecated fromRoot('config/kibana.yml'), - '/etc/kibana/kibana.yml', ].filter(isString); const DATA_PATHS = [ diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana index 7b411542986be..092dc6482fa1d 100644 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana @@ -11,3 +11,5 @@ nice="" KILL_ON_STOP_TIMEOUT=0 BABEL_CACHE_PATH="/var/lib/kibana/optimize/.babel_register_cache.json" + +KIBANA_PATH_CONF="/etc/kibana" diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index fc1b797fe8ed2..a17d15522b45e 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -23,6 +23,7 @@ pidfile="/var/run/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name +export KIBANA_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 From 96ef01828ce0a0083dad4ad4aa104feea9e6d60c Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Sat, 30 May 2020 09:51:07 +0200 Subject: [PATCH 30/38] [SIEM] Covers 'Import query from saved timeline' functionality with Cypress (#67459) * modifies 'Creates and activates a new custom rule' test to cover 'import query from saved timeline' functionality * adds missing files Co-authored-by: Elastic Machine --- .../signal_detection_rules_custom.spec.ts | 8 +- x-pack/plugins/siem/cypress/objects/rule.ts | 4 +- .../siem/cypress/screens/create_new_rule.ts | 3 + .../plugins/siem/cypress/screens/timeline.ts | 4 + .../siem/cypress/tasks/create_new_rule.ts | 11 + .../rules/step_define_rule/index.tsx | 5 +- .../custom_rule_with_timeline/data.json.gz | Bin 0 -> 67934 bytes .../custom_rule_with_timeline/mappings.json | 7983 +++++++++++++++++ 8 files changed, 8012 insertions(+), 6 deletions(-) create mode 100644 x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz create mode 100644 x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts index 4b5e12124dd40..04762bbf352d2 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts @@ -41,7 +41,7 @@ import { import { createAndActivateRule, fillAboutRuleAndContinue, - fillDefineCustomRuleAndContinue, + fillDefineCustomRuleWithImportedQueryAndContinue, } from '../tasks/create_new_rule'; import { goToManageSignalDetectionRules, @@ -66,11 +66,11 @@ import { DETECTIONS } from '../urls/navigation'; describe('Signal detection rules, custom', () => { before(() => { - esArchiverLoad('prebuilt_rules_loaded'); + esArchiverLoad('custom_rule_with_timeline'); }); after(() => { - esArchiverUnload('prebuilt_rules_loaded'); + esArchiverUnload('custom_rule_with_timeline'); }); it('Creates and activates a new custom rule', () => { @@ -80,7 +80,7 @@ describe('Signal detection rules, custom', () => { goToManageSignalDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); goToCreateNewRule(); - fillDefineCustomRuleAndContinue(newRule); + fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); createAndActivateRule(); diff --git a/x-pack/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/siem/cypress/objects/rule.ts index 7ce8aa69f3339..d750fe212002d 100644 --- a/x-pack/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/plugins/siem/cypress/objects/rule.ts @@ -28,6 +28,7 @@ export interface CustomRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; + timelineId: string; } export interface MachineLearningRule { @@ -56,7 +57,7 @@ const mitre2: Mitre = { }; export const newRule: CustomRule = { - customQuery: 'hosts.name: *', + customQuery: 'host.name: *', name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -66,6 +67,7 @@ export const newRule: CustomRule = { falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], note: '# test markdown', + timelineId: '352c6110-9ffb-11ea-b3d8-857d6042d9bd', }; export const machineLearningRule: MachineLearningRule = { diff --git a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts index db9866cdf7f63..bc0740554bc52 100644 --- a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts @@ -24,6 +24,9 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; +export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = + '[data-test-subj="importQueryFromSavedTimeline"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index 58d2568084f7c..ed1dc97454fb3 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -21,6 +21,10 @@ export const SEARCH_OR_FILTER_CONTAINER = export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; +export const TIMELINE = (id: string) => { + return `[data-test-subj="title-${id}"]`; +}; + export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS_EMPTY = diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts index 6324b42f3783a..eca5885e7b3d9 100644 --- a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts @@ -14,6 +14,7 @@ import { CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, + IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +31,7 @@ import { SEVERITY_DROPDOWN, TAGS_INPUT, } from '../screens/create_new_rule'; +import { TIMELINE } from '../screens/timeline'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); @@ -86,6 +88,15 @@ export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); + cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery); + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + + cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); +}; + export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRule) => { cy.get(MACHINE_LEARNING_DROPDOWN).click({ force: true }); cy.contains(MACHINE_LEARNING_LIST, rule.machineLearningJob).click(); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx index 119f851ecdfe4..fc875908bd4ef 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx @@ -203,7 +203,10 @@ const StepDefineRuleComponent: FC = ({ config={{ ...schema.queryBar, labelAppend: ( - + {i18n.IMPORT_TIMELINE_QUERY} ), diff --git a/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..3d50451cee39fe8c8f8b49da585473585a812c1d GIT binary patch literal 67934 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 literal 0 HcmV?d00001 diff --git a/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json new file mode 100644 index 0000000000000..d01e6344bcfaf --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json @@ -0,0 +1,7983 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "epm-packages": "92b4b1899b887b090d01c033f3118a85", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "864760267df6c970f629bd4458506c53", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "ingest-agent-configs": "d9a5cbdce8e937f674a7b376c47a34a1", + "ingest-datasources": "c0fe6347b0eebcbf421841669e3acd31", + "ingest-outputs": "0e57221778a7153c8292edf154099036", + "ingest_manager_settings": "c5b0749b4ab03c582efd4c14cb8f132c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "17ec409954864e592ceec0c5eae29ad9", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "dynamic": "false", + "type": "object" + }, + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "agent_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoPointFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoShapeFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-agent-configs": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-datasources": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "map": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "endpoint": { + "properties": { + "artifact": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "process": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "entry_modified": { + "type": "double" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "macro": { + "properties": { + "code_page": { + "type": "long" + }, + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "file_extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "stream": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code_size": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "quarantine_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "quarantine_result": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "temp_file_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "file": { + "properties": { + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 6a8b07fe8ea1d33d9377e289df250439a38de1a5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Sat, 30 May 2020 21:22:42 +0200 Subject: [PATCH 31/38] Fix visualize and lens telemetry (#67749) --- x-pack/plugins/lens/server/usage/task.ts | 3 ++- .../server/lib/tasks/visualizations/task_runner.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 5a5d26fa2afde..cde6e7eb6c090 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -6,6 +6,7 @@ import { APICaller, CoreSetup, Logger } from 'kibana/server'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import moment from 'moment'; import { RunContext, @@ -191,7 +192,7 @@ export function telemetryTaskRunner( return { async run() { - const kibanaIndex = (await config.toPromise()).kibana.index; + const kibanaIndex = (await config.pipe(first()).toPromise()).kibana.index; return Promise.all([ getDailyEvents(kibanaIndex, callCluster), diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index 346cc75bb9b24..b15ead36a75f6 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -6,6 +6,8 @@ import { Observable } from 'rxjs'; import _, { countBy, groupBy, mapValues } from 'lodash'; +import { first } from 'rxjs/operators'; + import { APICaller, IClusterClient } from 'src/core/server'; import { getNextMidnight } from '../../get_next_midnight'; import { TaskInstance } from '../../../../../task_manager/server'; @@ -80,7 +82,7 @@ export function visualizationsTaskRunner( let error; try { - const index = (await config.toPromise()).kibana.index; + const index = (await config.pipe(first()).toPromise()).kibana.index; stats = await getStats((await esClientPromise).callAsInternalUser, index); } catch (err) { if (err.constructor === Error) { From 96e0e911ea3a63e1d174d2f1583da59e609b3088 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Sat, 30 May 2020 18:52:01 -0600 Subject: [PATCH 32/38] [SIEM][Lists] Adds test mocks and README.md to the lists plugin ## Summary * https://github.com/elastic/kibana/issues/67675 * Adds README.md to the lists plugin * Adds the mocks to the server side of the lists plugin * Changes out the SIEM code to use the mocks now that they are within the plugin ### Checklist Delete any items that are not applicable to this PR. - [x] 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) - [x] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- x-pack/plugins/lists/README.md | 254 ++++++++++++++++++ .../exception_list_item_schema.mock.ts | 40 +++ .../response/exception_list_schema.mock.ts | 24 ++ .../found_exception_list_item_schema.mock.ts | 15 ++ .../found_exception_list_schema.mock.ts | 15 ++ .../response/found_list_item_schema.mock.ts | 16 ++ .../response/found_list_schema.mock.ts | 16 ++ .../lists/public/exceptions/__mocks__/api.ts | 15 +- .../lists/public/exceptions/api.test.ts | 33 ++- .../hooks/persist_exception_item.test.tsx | 6 +- .../hooks/persist_exception_list.test.tsx | 6 +- .../plugins/lists/public/exceptions/mock.ts | 52 ---- x-pack/plugins/lists/public/index.tsx | 7 +- x-pack/plugins/lists/server/get_user.test.ts | 13 +- x-pack/plugins/lists/server/mocks.ts | 23 ++ .../exception_list_client.mock.ts | 35 +++ .../exception_list_client.test.ts | 34 +++ .../server/services/lists/list_client.mock.ts | 69 +++++ .../server/services/lists/list_client.test.ts | 30 +++ .../lists/server/services/lists/types.ts | 2 +- .../services/utils/get_search_after_scroll.ts | 1 + .../services/utils/scroll_to_start_page.ts | 1 + .../plugins/siem/public/lists_plugin_deps.ts | 2 - .../signals/filter_events_with_list.test.ts | 98 +++---- .../signals/search_after_bulk_create.test.ts | 82 +++--- .../signals/search_after_bulk_create.ts | 2 +- .../signals/signal_rule_alert_type.test.ts | 9 +- 27 files changed, 690 insertions(+), 210 deletions(-) create mode 100644 x-pack/plugins/lists/README.md create mode 100644 x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/server/mocks.ts create mode 100644 x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts create mode 100644 x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts create mode 100644 x-pack/plugins/lists/server/services/lists/list_client.mock.ts create mode 100644 x-pack/plugins/lists/server/services/lists/list_client.test.ts diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md new file mode 100644 index 0000000000000..cb343c95b0103 --- /dev/null +++ b/x-pack/plugins/lists/README.md @@ -0,0 +1,254 @@ +README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + +The scripts rely on CURL and jq: + +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) + +Install curl and jq (mac instructions) + +```sh +brew update +brew install curl +brew install jq +``` + +Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL` +and add these environment variables: + +```sh +export ELASTICSEARCH_USERNAME=${user} +export ELASTICSEARCH_PASSWORD=${password} +export ELASTICSEARCH_URL=https://${ip}:9200 +export KIBANA_URL=http://localhost:5601 +export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} +export KIBANA_INDEX=.kibana-${your user id} +``` + +source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: + +```sh +source ~/.zshrc +``` + +Open your `kibana.dev.yml` file and add these lines: + +```sh +# Enable lists feature +xpack.lists.enabled: true +xpack.lists.listIndex: '.lists-frank' +xpack.lists.listItemIndex: '.items-frank' +``` + +Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will +get in the way of the CURL scripts written as is. + +Go to the scripts folder `cd kibana/x-pack/plugins/lists/server/scripts` and run: + +```sh +./hard_reset.sh +./post_list.sh +``` + +which will: + +- Delete any existing lists you have +- Delete any existing list items you have +- Delete any existing exception lists you have +- Delete any existing exception list items you have +- Delete any existing mapping, policies, and templates, you might have previously had. +- Add the latest list and list item index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.lists.listIndex` and `xpack.lists.listItemIndex`. +- Posts the sample list from `./lists/new/list_ip.json` + +Now you can run + +```sh +./post_list.sh +``` + +You should see the new list created like so: + +```sh +{ + "id": "list-ip", + "created_at": "2020-05-28T19:15:22.344Z", + "created_by": "yo", + "description": "This list describes bad internet ip", + "name": "Simple list with an ip", + "tie_breaker_id": "c57efbc4-4977-4a32-995f-cfd296bed521", + "type": "ip", + "updated_at": "2020-05-28T19:15:22.344Z", + "updated_by": "yo" +} +``` + +You can add a list item like so: + +```sh + ./post_list_item.sh +``` + +You should see the new list item created and attached to the above list like so: + +```sh +{ + "id": "hand_inserted_item_id", + "type": "ip", + "value": "127.0.0.1", + "created_at": "2020-05-28T19:15:49.790Z", + "created_by": "yo", + "list_id": "list-ip", + "tie_breaker_id": "a881bf2e-1e17-4592-bba8-d567cb07d234", + "updated_at": "2020-05-28T19:15:49.790Z", + "updated_by": "yo" +} +``` + +If you want to post an exception list it would be like so: + +```sh +./post_exception_list.sh +``` + +You should see the new exception list created like so: + +```sh +{ + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-05-28T19:16:31.052Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "id": "bcb94680-a117-11ea-ad9d-c71f4820e65b", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "86e08c8c-c970-4b08-a6e2-cdba7bb4e023", + "type": "endpoint", + "updated_at": "2020-05-28T19:16:31.080Z", + "updated_by": "yo" +} +``` + +And you can attach exception list items like so: + +```ts +{ + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "comment": [], + "created_at": "2020-05-28T19:17:21.099Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "match": "Elastic, N.V." + }, + { + "field": "event.category", + "operator": "included", + "match_any": [ + "process", + "malware" + ] + } + ], + "id": "da8d3b30-a117-11ea-ad9d-c71f4820e65b", + "item_id": "endpoint_list_item", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "21f84703-9476-4af8-a212-aad31e18dcb9", + "type": "simple", + "updated_at": "2020-05-28T19:17:21.123Z", + "updated_by": "yo" +} +``` + +You can then do find for each one like so: + +```sh +./find_lists.sh +``` + +```sh +{ + "cursor": "WzIwLFsiYzU3ZWZiYzQtNDk3Ny00YTMyLTk5NWYtY2ZkMjk2YmVkNTIxIl1d", + "data": [ + { + "id": "list-ip", + "created_at": "2020-05-28T19:15:22.344Z", + "created_by": "yo", + "description": "This list describes bad internet ip", + "name": "Simple list with an ip", + "tie_breaker_id": "c57efbc4-4977-4a32-995f-cfd296bed521", + "type": "ip", + "updated_at": "2020-05-28T19:15:22.344Z", + "updated_by": "yo" + } + ], + "page": 1, + "per_page": 20, + "total": 1 +} +``` + +or for finding exception lists: + +```sh +./find_exception_lists.sh +``` + +```sh +{ + "data": [ + { + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-05-28T19:16:31.052Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "id": "bcb94680-a117-11ea-ad9d-c71f4820e65b", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "86e08c8c-c970-4b08-a6e2-cdba7bb4e023", + "type": "endpoint", + "updated_at": "2020-05-28T19:16:31.080Z", + "updated_by": "yo" + } + ], + "page": 1, + "per_page": 20, + "total": 1 +} +``` + +See the full scripts folder for all the capabilities. diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..901715b601b80 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -0,0 +1,40 @@ +/* + * 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 { ExceptionListItemSchema } from './exception_list_item_schema'; + +export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ + _tags: ['endpoint', 'process', 'malware', 'os:linux'], + comment: [], + created_at: '2020-04-23T00:19:13.289Z', + created_by: 'user_name', + description: 'This is a sample endpoint type exception', + entries: [ + { + field: 'actingProcess.file.signer', + match: 'Elastic, N.V.', + match_any: undefined, + operator: 'included', + }, + { + field: 'event.category', + match: undefined, + match_any: ['process', 'malware'], + operator: 'included', + }, + ], + id: '1', + item_id: 'endpoint_list_item', + list_id: 'endpoint_list', + meta: {}, + name: 'Sample Endpoint Exception List', + namespace_type: 'single', + tags: ['user added string for a tag', 'malware'], + tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', + type: 'simple', + updated_at: '2020-04-23T00:19:13.289Z', + updated_by: 'user_name', +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts new file mode 100644 index 0000000000000..017b959a2baf3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionListSchema } from './exception_list_schema'; + +export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ + _tags: ['endpoint', 'process', 'malware', 'os:linux'], + created_at: '2020-04-23T00:19:13.289Z', + created_by: 'user_name', + description: 'This is a sample endpoint type exception', + id: '1', + list_id: 'endpoint_list', + meta: {}, + name: 'Sample Endpoint Exception List', + namespace_type: 'single', + tags: ['user added string for a tag', 'malware'], + tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', + type: 'endpoint', + updated_at: '2020-04-23T00:19:13.289Z', + updated_by: 'user_name', +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f760e602605ba --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; +import { FoundExceptionListItemSchema } from './found_exception_list_item_schema'; + +export const getFoundExceptionListItemSchemaMock = (): FoundExceptionListItemSchema => ({ + data: [getExceptionListItemSchemaMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts new file mode 100644 index 0000000000000..ce71a27dbc4d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListSchemaMock } from './exception_list_schema.mock'; +import { FoundExceptionListSchema } from './found_exception_list_schema'; + +export const getFoundExceptionListSchemaMock = (): FoundExceptionListSchema => ({ + data: [getExceptionListSchemaMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts new file mode 100644 index 0000000000000..e96188c619d78 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { FoundListItemSchema } from './found_list_item_schema'; +import { getListItemResponseMock } from './list_item_schema.mock'; + +export const getFoundListItemSchemaMock = (): FoundListItemSchema => ({ + cursor: '123', + data: [getListItemResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts new file mode 100644 index 0000000000000..63d6a3b220ac1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { FoundListSchema } from './found_list_schema'; +import { getListResponseMock } from './list_schema.mock'; + +export const getFoundListSchemaMock = (): FoundListSchema => ({ + cursor: '123', + data: [getListResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts index f624189915dcf..787d374ab2cad 100644 --- a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts +++ b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -15,37 +17,38 @@ import { ApiCallByIdProps, ApiCallByListIdProps, } from '../types'; -import { mockExceptionItem, mockExceptionList } from '../mock'; /* eslint-disable @typescript-eslint/no-unused-vars */ export const addExceptionList = async ({ http, list, signal, -}: AddExceptionListProps): Promise => Promise.resolve(mockExceptionList); +}: AddExceptionListProps): Promise => + Promise.resolve(getExceptionListSchemaMock()); export const addExceptionListItem = async ({ http, listItem, signal, }: AddExceptionListItemProps): Promise => - Promise.resolve(mockExceptionItem); + Promise.resolve(getExceptionListItemSchemaMock()); export const fetchExceptionListById = async ({ http, id, signal, -}: ApiCallByIdProps): Promise => Promise.resolve(mockExceptionList); +}: ApiCallByIdProps): Promise => Promise.resolve(getExceptionListSchemaMock()); export const fetchExceptionListItemsByListId = async ({ http, listId, signal, }: ApiCallByListIdProps): Promise => - Promise.resolve({ data: [mockExceptionItem], page: 1, per_page: 20, total: 1 }); + Promise.resolve({ data: [getExceptionListItemSchemaMock()], page: 1, per_page: 20, total: 1 }); export const fetchExceptionListItemById = async ({ http, id, signal, -}: ApiCallByIdProps): Promise => Promise.resolve(mockExceptionItem); +}: ApiCallByIdProps): Promise => + Promise.resolve(getExceptionListItemSchemaMock()); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 3a61140e5621d..18a89071e9887 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { createKibanaCoreStartMock } from '../common/mocks/kibana_core'; +import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; -import { - mockExceptionItem, - mockExceptionList, - mockNewExceptionItem, - mockNewExceptionList, -} from './mock'; +import { mockNewExceptionItem, mockNewExceptionList } from './mock'; import { addExceptionList, addExceptionListItem, @@ -43,7 +40,7 @@ describe('Exceptions Lists API', () => { describe('addExceptionList', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body', async () => { @@ -63,7 +60,7 @@ describe('Exceptions Lists API', () => { test('check parameter url, body when "list.id" exists', async () => { await addExceptionList({ http: mockKibanaHttpService(), - list: mockExceptionList, + list: getExceptionListSchemaMock(), signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { @@ -80,14 +77,14 @@ describe('Exceptions Lists API', () => { list: mockNewExceptionList, signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); describe('addExceptionListItem', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionItem); + fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body', async () => { @@ -107,7 +104,7 @@ describe('Exceptions Lists API', () => { test('check parameter url, body when "listItem.id" exists', async () => { await addExceptionListItem({ http: mockKibanaHttpService(), - listItem: mockExceptionItem, + listItem: getExceptionListItemSchemaMock(), signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { @@ -124,14 +121,14 @@ describe('Exceptions Lists API', () => { listItem: mockNewExceptionItem, signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionItem); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); describe('fetchExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body', async () => { @@ -155,7 +152,7 @@ describe('Exceptions Lists API', () => { id: '1', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); @@ -224,7 +221,7 @@ describe('Exceptions Lists API', () => { describe('deleteExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { @@ -248,14 +245,14 @@ describe('Exceptions Lists API', () => { id: '1', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); describe('deleteExceptionListItemById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionItem); + fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { @@ -279,7 +276,7 @@ describe('Exceptions Lists API', () => { id: '1', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionItem); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx index 098ee1f81f492..b78ad250b8910 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx @@ -6,7 +6,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { mockExceptionItem } from '../mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item'; @@ -32,7 +32,7 @@ describe('usePersistExceptionItem', () => { () => usePersistExceptionItem({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionItem); + result.current[1](getExceptionListItemSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -45,7 +45,7 @@ describe('usePersistExceptionItem', () => { usePersistExceptionItem({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionItem); + result.current[1](getExceptionListItemSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx index 5cad95a38dbec..605dd635aa4f5 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx @@ -6,7 +6,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { mockExceptionList } from '../mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list'; @@ -32,7 +32,7 @@ describe('usePersistExceptionList', () => { () => usePersistExceptionList({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionList); + result.current[1](getExceptionListSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -45,7 +45,7 @@ describe('usePersistExceptionList', () => { usePersistExceptionList({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionList); + result.current[1](getExceptionListSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/lists/public/exceptions/mock.ts b/x-pack/plugins/lists/public/exceptions/mock.ts index 38a0e65992982..fd06dac65c6fb 100644 --- a/x-pack/plugins/lists/public/exceptions/mock.ts +++ b/x-pack/plugins/lists/public/exceptions/mock.ts @@ -6,27 +6,8 @@ import { CreateExceptionListItemSchemaPartial, CreateExceptionListSchemaPartial, - ExceptionListItemSchema, - ExceptionListSchema, } from '../../common/schemas'; -export const mockExceptionList: ExceptionListSchema = { - _tags: ['endpoint', 'process', 'malware', 'os:linux'], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', - id: '1', - list_id: 'endpoint_list', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', - tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'endpoint', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', -}; - export const mockNewExceptionList: CreateExceptionListSchemaPartial = { _tags: ['endpoint', 'process', 'malware', 'os:linux'], description: 'This is a sample endpoint type exception', @@ -59,36 +40,3 @@ export const mockNewExceptionItem: CreateExceptionListItemSchemaPartial = { tags: ['user added string for a tag', 'malware'], type: 'simple', }; - -export const mockExceptionItem: ExceptionListItemSchema = { - _tags: ['endpoint', 'process', 'malware', 'os:linux'], - comment: [], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', - entries: [ - { - field: 'actingProcess.file.signer', - match: 'Elastic, N.V.', - match_any: undefined, - operator: 'included', - }, - { - field: 'event.category', - match: undefined, - match_any: ['process', 'malware'], - operator: 'included', - }, - ], - id: '1', - item_id: 'endpoint_list_item', - list_id: 'endpoint_list', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', - tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'simple', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', -}; diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index b23f31abd4d87..fb4d5de06ae54 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -7,9 +7,4 @@ export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionList } from './exceptions/hooks/use_exception_list'; -export { - mockExceptionItem, - mockExceptionList, - mockNewExceptionItem, - mockNewExceptionList, -} from './exceptions/mock'; +export { mockNewExceptionItem, mockNewExceptionList } from './exceptions/mock'; diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts index 0992e3c361fcf..a1c78f5ea4684 100644 --- a/x-pack/plugins/lists/server/get_user.test.ts +++ b/x-pack/plugins/lists/server/get_user.test.ts @@ -8,7 +8,6 @@ import { httpServerMock } from 'src/core/server/mocks'; import { KibanaRequest } from 'src/core/server'; import { securityMock } from '../../security/server/mocks'; -import { SecurityPluginSetup } from '../../security/server'; import { getUser } from './get_user'; @@ -24,42 +23,42 @@ describe('get_user', () => { }); test('it returns "bob" as the user given a security request with "bob"', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' }); const user = getUser({ request, security }); expect(user).toEqual('bob'); }); test('it returns "alice" as the user given a security request with "alice"', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' }); const user = getUser({ request, security }); expect(user).toEqual('alice'); }); test('it returns "elastic" as the user given null as the current user', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(null); const user = getUser({ request, security }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given undefined as the current user', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given undefined as the plugin', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security: undefined }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given null as the plugin', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security: null }); expect(user).toEqual('elastic'); diff --git a/x-pack/plugins/lists/server/mocks.ts b/x-pack/plugins/lists/server/mocks.ts new file mode 100644 index 0000000000000..aad4a25a900a1 --- /dev/null +++ b/x-pack/plugins/lists/server/mocks.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListPluginSetup } from './types'; +import { getListClientMock } from './services/lists/list_client.mock'; +import { getExceptionListClientMock } from './services/exception_lists/exception_list_client.mock'; + +const createSetupMock = (): jest.Mocked => { + const mock: jest.Mocked = { + getExceptionListClient: jest.fn().mockReturnValue(getExceptionListClientMock()), + getListClient: jest.fn().mockReturnValue(getListClientMock()), + }; + return mock; +}; + +export const listMock = { + createSetup: createSetupMock, + getExceptionList: getExceptionListClientMock, + getListClient: getListClientMock, +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts new file mode 100644 index 0000000000000..d0e238f8c5c40 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts @@ -0,0 +1,35 @@ +/* + * 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 { getFoundExceptionListSchemaMock } from '../../../common/schemas/response/found_exception_list_schema.mock'; +import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { ExceptionListClient } from './exception_list_client'; + +export class ExceptionListClientMock extends ExceptionListClient { + public getExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public getExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public createExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public updateExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public deleteExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public createExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public updateExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public deleteExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()); + public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock()); +} + +export const getExceptionListClientMock = (): ExceptionListClient => { + const mock = new ExceptionListClientMock({ + savedObjectsClient: savedObjectsClientMock.create(), + user: 'elastic', + }); + return mock; +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts new file mode 100644 index 0000000000000..f91331f5b4308 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { getExceptionListClientMock } from './exception_list_client.mock'; + +describe('exception_list_client', () => { + describe('Mock client sanity checks', () => { + test('it returns the exception list as expected', async () => { + const mock = getExceptionListClientMock(); + const list = await mock.getExceptionList({ + id: '123', + listId: '123', + namespaceType: 'single', + }); + expect(list).toEqual(getExceptionListSchemaMock()); + }); + + test('it returns the the exception list item as expected', async () => { + const mock = getExceptionListClientMock(); + const listItem = await mock.getExceptionListItem({ + id: '123', + itemId: '123', + namespaceType: 'single', + }); + expect(listItem).toEqual(getExceptionListItemSchemaMock()); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts new file mode 100644 index 0000000000000..43a01a3ca62dc --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts @@ -0,0 +1,69 @@ +/* + * 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 { getFoundListItemSchemaMock } from '../../../common/schemas/response/found_list_item_schema.mock'; +import { getFoundListSchemaMock } from '../../../common/schemas/response/found_list_schema.mock'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + +import { ListClient } from './list_client'; + +export class ListClientMock extends ListClient { + public getListIndex = jest.fn().mockReturnValue(LIST_INDEX); + public getListItemIndex = jest.fn().mockReturnValue(LIST_ITEM_INDEX); + public getList = jest.fn().mockResolvedValue(getListResponseMock()); + public createList = jest.fn().mockResolvedValue(getListResponseMock()); + public createListIfItDoesNotExist = jest.fn().mockResolvedValue(getListResponseMock()); + public getListIndexExists = jest.fn().mockResolvedValue(true); + public getListItemIndexExists = jest.fn().mockResolvedValue(true); + public createListBootStrapIndex = jest.fn().mockResolvedValue({}); + public createListItemBootStrapIndex = jest.fn().mockResolvedValue({}); + public getListPolicyExists = jest.fn().mockResolvedValue(true); + public getListItemPolicyExists = jest.fn().mockResolvedValue(true); + public getListTemplateExists = jest.fn().mockResolvedValue(true); + public getListItemTemplateExists = jest.fn().mockResolvedValue(true); + public getListTemplate = jest.fn().mockResolvedValue({}); + public getListItemTemplate = jest.fn().mockResolvedValue({}); + public setListTemplate = jest.fn().mockResolvedValue({}); + public setListItemTemplate = jest.fn().mockResolvedValue({}); + public setListPolicy = jest.fn().mockResolvedValue({}); + public setListItemPolicy = jest.fn().mockResolvedValue({}); + public deleteListIndex = jest.fn().mockResolvedValue(true); + public deleteListItemIndex = jest.fn().mockResolvedValue(true); + public deleteListPolicy = jest.fn().mockResolvedValue({}); + public deleteListItemPolicy = jest.fn().mockResolvedValue({}); + public deleteListTemplate = jest.fn().mockResolvedValue({}); + public deleteListItemTemplate = jest.fn().mockResolvedValue({}); + public deleteListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public deleteListItemByValue = jest.fn().mockResolvedValue(getListItemResponseMock()); + public deleteList = jest.fn().mockResolvedValue(getListResponseMock()); + public exportListItemsToStream = jest.fn().mockResolvedValue(undefined); + public importListItemsToStream = jest.fn().mockResolvedValue(undefined); + public getListItemByValue = jest.fn().mockResolvedValue([getListItemResponseMock()]); + public createListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public updateListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public updateList = jest.fn().mockResolvedValue(getListResponseMock()); + public getListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public getListItemByValues = jest.fn().mockResolvedValue([getListItemResponseMock()]); + public findList = jest.fn().mockResolvedValue(getFoundListSchemaMock()); + public findListItem = jest.fn().mockResolvedValue(getFoundListItemSchemaMock()); +} + +export const getListClientMock = (): ListClient => { + const mock = new ListClientMock({ + callCluster: getCallClusterMock(), + config: { + enabled: true, + listIndex: LIST_INDEX, + listItemIndex: LIST_ITEM_INDEX, + }, + spaceId: 'default', + user: 'elastic', + }); + return mock; +}; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.test.ts b/x-pack/plugins/lists/server/services/lists/list_client.test.ts new file mode 100644 index 0000000000000..0c3a58283ffe2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_client.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + +import { getListClientMock } from './list_client.mock'; + +describe('list_client', () => { + describe('Mock client sanity checks', () => { + test('it returns the get list index as expected', () => { + const mock = getListClientMock(); + expect(mock.getListIndex()).toEqual(LIST_INDEX); + }); + + test('it returns the get list item index as expected', () => { + const mock = getListClientMock(); + expect(mock.getListItemIndex()).toEqual(LIST_ITEM_INDEX); + }); + + test('it returns a mock list item', async () => { + const mock = getListClientMock(); + const listItem = await mock.getListItem({ id: '123' }); + expect(listItem).toEqual(getListItemResponseMock()); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/types.ts b/x-pack/plugins/lists/server/services/lists/types.ts index 2e0e4b7d038e7..47419929c43a6 100644 --- a/x-pack/plugins/lists/server/services/lists/types.ts +++ b/x-pack/plugins/lists/server/services/lists/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Scroll { +export interface Scroll { searchAfter: string[] | undefined; validSearchAfterFound: boolean; } diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts index 9721baefbe5ee..2af501106d659 100644 --- a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -7,6 +7,7 @@ import { APICaller } from 'kibana/server'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; +import { Scroll } from '../lists/types'; import { getQueryFilter } from './get_query_filter'; import { getSortWithTieBreaker } from './get_sort_with_tie_breaker'; diff --git a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts index 16e07044dc0d4..6b898a54bb9fe 100644 --- a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts +++ b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts @@ -7,6 +7,7 @@ import { APICaller } from 'kibana/server'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; +import { Scroll } from '../lists/types'; import { calculateScrollMath } from './calculate_scroll_math'; import { getSearchAfterScroll } from './get_search_after_scroll'; diff --git a/x-pack/plugins/siem/public/lists_plugin_deps.ts b/x-pack/plugins/siem/public/lists_plugin_deps.ts index b99e5015c716a..d2ee5ae56b7d9 100644 --- a/x-pack/plugins/siem/public/lists_plugin_deps.ts +++ b/x-pack/plugins/siem/public/lists_plugin_deps.ts @@ -8,8 +8,6 @@ export { useExceptionList, usePersistExceptionItem, usePersistExceptionList, - mockExceptionItem, - mockExceptionList, mockNewExceptionItem, mockNewExceptionList, } from '../../lists/public'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 86efdb6603493..d56e167f59e4c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -8,17 +8,23 @@ import uuid from 'uuid'; import { filterEventsAgainstList } from './filter_events_with_list'; import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results'; -import { ListClient } from '../../../../../lists/server'; +import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; +import { listMock } from '../../../../../lists/server/mocks'; const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); describe('filterEventsAgainstList', () => { + let listClient = listMock.getListClient(); + beforeEach(() => { + jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.getListItemByValues = jest.fn().mockResolvedValue([]); + }); + it('should respond with eventSearchResult if exceptionList is empty', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: undefined, eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', @@ -35,9 +41,7 @@ describe('filterEventsAgainstList', () => { try { await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -66,9 +70,7 @@ describe('filterEventsAgainstList', () => { try { await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -101,9 +103,7 @@ describe('filterEventsAgainstList', () => { it('should respond with same list if no items match value list', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -122,27 +122,17 @@ describe('filterEventsAgainstList', () => { expect(res.hits.hits.length).toEqual(4); }); it('should respond with less items in the list if some values match', async () => { - let outerType = ''; - let outerListId = ''; + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async ({ - value, - type, - listId, - }: { - type: string; - listId: string; - value: string[]; - }) => { - outerType = type; - outerListId = listId; - return value.slice(0, 2).map((item) => ({ - value: item, - })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -163,8 +153,10 @@ describe('filterEventsAgainstList', () => { '7.7.7.7', ]), }); - expect(outerType).toEqual('ip'); - expect(outerListId).toEqual('ci-badguys.txt'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( + 'ci-badguys.txt' + ); expect(res.hits.hits.length).toEqual(2); }); }); @@ -172,9 +164,7 @@ describe('filterEventsAgainstList', () => { it('should respond with empty list if no items match value list', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -193,27 +183,17 @@ describe('filterEventsAgainstList', () => { expect(res.hits.hits.length).toEqual(0); }); it('should respond with less items in the list if some values match', async () => { - let outerType = ''; - let outerListId = ''; + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async ({ - value, - type, - listId, - }: { - type: string; - listId: string; - value: string[]; - }) => { - outerType = type; - outerListId = listId; - return value.slice(0, 2).map((item) => ({ - value: item, - })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -234,8 +214,10 @@ describe('filterEventsAgainstList', () => { '7.7.7.7', ]), }); - expect(outerType).toEqual('ip'); - expect(outerListId).toEqual('ci-badguys.txt'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( + 'ci-badguys.txt' + ); expect(res.hits.hits.length).toEqual(2); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 7479ab54af6e6..a306a016b4205 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -15,15 +15,18 @@ import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import uuid from 'uuid'; -import { ListClient } from '../../../../../lists/server'; -import { ListItemArraySchema } from '../../../../../lists/common/schemas'; +import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; +import { listMock } from '../../../../../lists/server/mocks'; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; let inputIndexPattern: string[] = []; + let listClient = listMock.getListClient(); const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); beforeEach(() => { jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.getListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createAlertServices(); }); @@ -93,9 +96,7 @@ describe('searchAfterAndBulkCreate', () => { }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -169,9 +170,7 @@ describe('searchAfterAndBulkCreate', () => { }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -243,19 +242,18 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); + + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - listClient: ({ - getListItemByValues: async ({ - value, - }: { - type: string; - listId: string; - value: string[]; - }) => { - return value.map((item) => ({ value: item })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: undefined, services: mockService, logger: mockLogger, @@ -288,11 +286,7 @@ describe('searchAfterAndBulkCreate', () => { .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockRejectedValue(new Error('bulk failed')); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient: ({ - getListItemByValues: async () => { - return ([] as unknown) as ListItemArraySchema; - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -335,18 +329,16 @@ describe('searchAfterAndBulkCreate', () => { test('should return success with 0 total hits', async () => { const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient: ({ - getListItemByValues: async ({ - value, - }: { - type: string; - listId: string; - value: string[]; - }) => { - return value.map((item) => ({ value: item })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -405,18 +397,16 @@ describe('searchAfterAndBulkCreate', () => { .mockImplementation(() => { throw Error('Fake Error'); }); + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient: ({ - getListItemByValues: async ({ - value, - }: { - type: string; - listId: string; - value: string[]; - }) => { - return value.map((item) => ({ value: item })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index 05cdccedbc2c1..59c685ec3e815 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -81,7 +81,7 @@ export const searchAfterAndBulkCreate = async ({ let sortId; // tells us where to start our next search_after query let searchResultSize = 0; - /* + /* The purpose of `maxResults` is to ensure we do not perform extra search_after's. This will be reset on each iteration, although it really only matters for the first diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index ea7255b8a925a..8e7034b006327 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -17,7 +17,7 @@ import { scheduleNotificationActions } from '../notifications/schedule_notificat import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; -import { ListPluginSetup } from '../../../../../lists/server/types'; +import { listMock } from '../../../../../lists/server/mocks'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -69,11 +69,6 @@ describe('rules_notification_alert_type', () => { modulesProvider: jest.fn(), resultsServiceProvider: jest.fn(), }; - const listMock = { - getListClient: () => ({ - getListItemByValues: () => [], - }), - }; let payload: jest.Mocked; let alert: ReturnType; let logger: ReturnType; @@ -116,7 +111,7 @@ describe('rules_notification_alert_type', () => { logger, version, ml: mlMock, - lists: (listMock as unknown) as ListPluginSetup, + lists: listMock.createSetup(), }); }); From 6753b1d36bc567a8948f24d600fd439b8125acb8 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 1 Jun 2020 14:34:53 +0200 Subject: [PATCH 33/38] Added autocompletion for update by query (#67741) Co-authored-by: Elastic Machine --- .../json/overrides/update_by_query.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json new file mode 100644 index 0000000000000..44819eda6e29e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json @@ -0,0 +1,17 @@ +{ + "update_by_query": { + "data_autocomplete_rules": { + "conflicts": "", + "query": { + "__scope_link": "GLOBAL.query" + }, + "script": { + "__template": { + "source": "", + "lang": "painless" + }, + "__scope_link": "GLOBAL.script" + } + } + } +} From df4615a392aa830eed4811f33478729a2df2405f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 1 Jun 2020 08:43:36 -0400 Subject: [PATCH 34/38] [ILM] Fix fetch policies query (#67827) --- .../public/application/services/api.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js index 386df63111a89..6b46d6e6ea735 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js @@ -28,8 +28,7 @@ export async function loadIndexTemplates() { } export async function loadPolicies(withIndices) { - const query = withIndices ? '?withIndices=true' : ''; - return await sendGet('policies', query); + return await sendGet('policies', { withIndices }); } export async function savePolicy(policy) { From 773a44defa91d4cfdcd209760d76f9d715eafe3f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 1 Jun 2020 08:44:38 -0400 Subject: [PATCH 35/38] [Component templates] Server side (#66596) --- .../server/client/elasticsearch.ts | 63 ++++ .../plugins/index_management/server/plugin.ts | 49 ++- .../routes/api/component_templates/create.ts | 79 +++++ .../routes/api/component_templates/delete.ts | 51 +++ .../routes/api/component_templates/get.ts | 77 +++++ .../routes/api/component_templates/index.ts | 19 ++ .../component_templates/schema_validation.ts | 16 + .../routes/api/component_templates/update.ts | 62 ++++ .../index_management/server/routes/index.ts | 2 + .../server/services/license.ts | 4 +- .../index_management/component_templates.ts | 296 ++++++++++++++++++ .../apis/management/index_management/index.js | 1 + .../index_management/lib/elasticsearch.js | 10 + .../api_integration/services/legacy_es.js | 5 +- 14 files changed, 727 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/index_management/server/client/elasticsearch.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/create.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/get.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/index.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/update.ts create mode 100644 x-pack/test/api_integration/apis/management/index_management/component_templates.ts diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts new file mode 100644 index 0000000000000..65bd5411a249b --- /dev/null +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -0,0 +1,63 @@ +/* + * 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 elasticsearchJsPlugin = (Client: any, config: any, components: any) => { + const ca = components.clientAction.factory; + + Client.prototype.dataManagement = components.clientAction.namespaceFactory(); + const dataManagement = Client.prototype.dataManagement.prototype; + + dataManagement.getComponentTemplates = ca({ + urls: [ + { + fmt: '/_component_template', + }, + ], + method: 'GET', + }); + + dataManagement.getComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + dataManagement.saveComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'PUT', + }); + + dataManagement.deleteComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); +}; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index e5bd7451b028f..f254333007c39 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -3,14 +3,33 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +declare module 'kibana/server' { + interface RequestHandlerContext { + dataManagement?: DataManagementContext; + } +} + import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server'; +import { + CoreSetup, + Plugin, + Logger, + PluginInitializerContext, + IScopedClusterClient, + ICustomClusterClient, +} from 'src/core/server'; import { PLUGIN } from '../common'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { License, IndexDataEnricher } from './services'; import { isEsError } from './lib/is_es_error'; +import { elasticsearchJsPlugin } from './client/elasticsearch'; + +export interface DataManagementContext { + client: IScopedClusterClient; +} export interface IndexManagementPluginSetup { indexDataEnricher: { @@ -18,11 +37,18 @@ export interface IndexManagementPluginSetup { }; } +async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { + const [core] = await getStartServices(); + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + return core.elasticsearch.legacy.createClient('dataManagement', esClientConfig); +} + export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly license: License; private readonly logger: Logger; private readonly indexDataEnricher: IndexDataEnricher; + private dataManagementESClient?: ICustomClusterClient; constructor(initContext: PluginInitializerContext) { this.logger = initContext.logger.get(); @@ -31,7 +57,10 @@ export class IndexMgmtServerPlugin implements Plugin { + this.dataManagementESClient = + this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); + + return { + client: this.dataManagementESClient.asScoped(request), + }; + }); + this.apiRoutes.setup({ router, license: this.license, @@ -65,5 +103,10 @@ export class IndexMgmtServerPlugin implements Plugin { + router.post( + { + path: addBasePath('/component_templates'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + const { name, ...componentTemplateDefinition } = req.body; + + try { + // Check that a component template with the same name doesn't already exist + const componentTemplateResponse = await callAsCurrentUser( + 'dataManagement.getComponentTemplate', + { name } + ); + + const { component_templates: componentTemplates } = componentTemplateResponse; + + if (componentTemplates.length) { + return res.conflict({ + body: new Error( + i18n.translate('xpack.idxMgmt.componentTemplates.createRoute.duplicateErrorMessage', { + defaultMessage: "There is already a component template with name '{name}'.", + values: { + name, + }, + }) + ), + }); + } + } catch (e) { + // Silently swallow error + } + + try { + const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + name, + body: componentTemplateDefinition, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts new file mode 100644 index 0000000000000..9e11967202b9c --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const paramsSchema = schema.object({ + names: schema.string(), +}); + +export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { + router.delete( + { + path: addBasePath('/component_templates/{names}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { names } = req.params; + const componentNames = names.split(','); + + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + componentNames.map((componentName) => { + return callAsCurrentUser('dataManagement.deleteComponentTemplate', { + name: componentName, + }) + .then(() => response.itemsDeleted.push(componentName)) + .catch((e) => + response.errors.push({ + name: componentName, + error: e, + }) + ); + }) + ); + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts new file mode 100644 index 0000000000000..87aa64421624e --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -0,0 +1,77 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { + // Get all component templates + router.get( + { path: addBasePath('/component_templates'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + try { + const response = await callAsCurrentUser('dataManagement.getComponentTemplates'); + + return res.ok({ body: response.component_templates }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); + + // Get single component template + router.get( + { + path: addBasePath('/component_templates/{name}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { name } = req.params; + + try { + const { component_templates: componentTemplates } = await callAsCurrentUser( + 'dataManagement.getComponentTemplates', + { + name, + } + ); + + return res.ok({ + body: { + ...componentTemplates[0], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts new file mode 100644 index 0000000000000..7ecb71182e87e --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; + +import { registerGetAllRoute } from './get'; +import { registerCreateRoute } from './create'; +import { registerUpdateRoute } from './update'; +import { registerDeleteRoute } from './delete'; + +export function registerComponentTemplateRoutes(dependencies: RouteDependencies) { + registerGetAllRoute(dependencies); + registerCreateRoute(dependencies); + registerUpdateRoute(dependencies); + registerDeleteRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts new file mode 100644 index 0000000000000..7d32637c6b977 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -0,0 +1,16 @@ +/* + * 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 componentTemplateSchema = { + template: schema.object({ + settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), + mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }), + version: schema.maybe(schema.number()), + _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts new file mode 100644 index 0000000000000..7e447bb110c67 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; +import { componentTemplateSchema } from './schema_validation'; + +const bodySchema = schema.object(componentTemplateSchema); + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: addBasePath('/component_templates/{name}'), + validate: { + body: bodySchema, + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { name } = req.params; + const { template, version, _meta } = req.body; + + try { + // Verify component exists; ES will throw 404 if not + await callAsCurrentUser('dataManagement.getComponentTemplate', { name }); + + const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + name, + body: { + template, + version, + _meta, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index 870cfa36ecc6a..1e5aaf8087624 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -11,6 +11,7 @@ import { registerTemplateRoutes } from './api/templates'; import { registerMappingRoute } from './api/mapping'; import { registerSettingsRoutes } from './api/settings'; import { registerStatsRoute } from './api/stats'; +import { registerComponentTemplateRoutes } from './api/component_templates'; export class ApiRoutes { setup(dependencies: RouteDependencies) { @@ -19,6 +20,7 @@ export class ApiRoutes { registerSettingsRoutes(dependencies); registerStatsRoute(dependencies); registerMappingRoute(dependencies); + registerComponentTemplateRoutes(dependencies); } start() {} diff --git a/x-pack/plugins/index_management/server/services/license.ts b/x-pack/plugins/index_management/server/services/license.ts index 2d863e283d440..9b68acd073c4a 100644 --- a/x-pack/plugins/index_management/server/services/license.ts +++ b/x-pack/plugins/index_management/server/services/license.ts @@ -53,12 +53,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = license.getStatus(); diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts new file mode 100644 index 0000000000000..a33e82ad9f79d --- /dev/null +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +// @ts-ignore +import { initElasticsearchHelpers } from './lib'; +// @ts-ignore +import { API_BASE_PATH } from './constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + const { createComponentTemplate, deleteComponentTemplate } = initElasticsearchHelpers(es); + + describe('Component templates', function () { + describe('Get', () => { + const COMPONENT_NAME = 'test_component_template'; + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }; + + before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); + after(() => deleteComponentTemplate(COMPONENT_NAME)); + + describe('all component templates', () => { + it('should return an array of component templates', async () => { + const { body: componentTemplates } = await supertest + .get(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + const testComponentTemplate = componentTemplates.find( + ({ name }: { name: string }) => name === COMPONENT_NAME + ); + + expect(testComponentTemplate).to.eql({ + name: COMPONENT_NAME, + component_template: COMPONENT, + }); + }); + }); + + describe('one component template', () => { + it('should return a single component template', async () => { + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + name: COMPONENT_NAME, + component_template: { + ...COMPONENT, + }, + }); + }); + }); + }); + + describe('Create', () => { + const COMPONENT_NAME = 'test_create_component_template'; + const REQUIRED_FIELDS_COMPONENT_NAME = 'test_create_required_fields_component_template'; + + after(() => { + deleteComponentTemplate(COMPONENT_NAME); + deleteComponentTemplate(REQUIRED_FIELDS_COMPONENT_NAME); + }); + + it('should create a component template', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .send({ + name: COMPONENT_NAME, + version: 1, + template: { + settings: { + number_of_shards: 1, + }, + aliases: { + alias1: {}, + }, + mappings: { + properties: { + host_name: { + type: 'keyword', + }, + }, + }, + }, + _meta: { + description: 'set number of shards to one', + serialization: { + class: 'MyComponentTemplate', + id: 10, + }, + }, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should create a component template with only required fields', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + // Excludes version and _meta fields + .send({ + name: REQUIRED_FIELDS_COMPONENT_NAME, + template: {}, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of a component template with the same name of an existing one', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .send({ + name: COMPONENT_NAME, + template: {}, + }) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a component template with name '${COMPONENT_NAME}'.`, + }); + }); + }); + + describe('Update', () => { + const COMPONENT_NAME = 'test_component_template'; + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }; + + before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); + after(() => deleteComponentTemplate(COMPONENT_NAME)); + + it('should allow an existing component template to be updated', async () => { + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...COMPONENT, + version: 1, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing component template to be updated', async () => { + const uri = `${API_BASE_PATH}/component_templates/component_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...COMPONENT, + version: 1, + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: + '[resource_not_found_exception] component template matching [component_does_not_exist] not found', + }); + }); + }); + + describe('Delete', () => { + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + }, + }; + + it('should delete a component template', async () => { + // Create component template to be deleted + const COMPONENT_NAME = 'test_delete_component_template'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + itemsDeleted: [COMPONENT_NAME], + errors: [], + }); + }); + + it('should delete multiple component templates', async () => { + // Create component templates to be deleted + const COMPONENT_ONE_NAME = 'test_delete_component_1'; + const COMPONENT_TWO_NAME = 'test_delete_component_2'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); + createComponentTemplate({ body: COMPONENT, name: COMPONENT_TWO_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_TWO_NAME}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each name instead + [COMPONENT_ONE_NAME, COMPONENT_TWO_NAME].forEach((componentName) => { + expect(itemsDeleted.includes(componentName)).to.be(true); + }); + }); + + it('should return an error for any component templates not sucessfully deleted', async () => { + const COMPONENT_DOES_NOT_EXIST = 'component_does_not_exist'; + + // Create component template to be deleted + const COMPONENT_ONE_NAME = 'test_delete_component_1'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_DOES_NOT_EXIST}`; + + const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body.itemsDeleted).to.eql([COMPONENT_ONE_NAME]); + expect(body.errors[0].name).to.eql(COMPONENT_DOES_NOT_EXIST); + expect(body.errors[0].error.msg).to.contain('index_template_missing_exception'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/index_management/index.js b/x-pack/test/api_integration/apis/management/index_management/index.js index cd3e27f9f7a61..fdee325938ff4 100644 --- a/x-pack/test/api_integration/apis/management/index_management/index.js +++ b/x-pack/test/api_integration/apis/management/index_management/index.js @@ -11,5 +11,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./templates')); + loadTestFile(require.resolve('./component_templates')); }); } diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js index 78aed8142eeba..b950a56a913db 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js @@ -34,6 +34,14 @@ export const initElasticsearchHelpers = (es) => { const catTemplate = (name) => es.cat.templates({ name, format: 'json' }); + const createComponentTemplate = (componentTemplate) => { + return es.dataManagement.saveComponentTemplate(componentTemplate); + }; + + const deleteComponentTemplate = (componentTemplateName) => { + return es.dataManagement.deleteComponentTemplate({ name: componentTemplateName }); + }; + return { createIndex, deleteIndex, @@ -42,5 +50,7 @@ export const initElasticsearchHelpers = (es) => { indexStats, cleanUp, catTemplate, + createComponentTemplate, + deleteComponentTemplate, }; }; diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 12a1576f78982..0ea061365aca2 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -8,7 +8,8 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import { elasticsearchClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; +import { elasticsearchClientPlugin as securityEsClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; +import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -19,6 +20,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [elasticsearchClientPlugin], + plugins: [securityEsClientPlugin, indexManagementEsClientPlugin], }); } From afbbafb0cfb556cda333d0b58dd81e8e424a5a6e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 1 Jun 2020 09:36:52 -0400 Subject: [PATCH 36/38] Fix support for `xpack.spaces.maxSpaces` (#67846) --- x-pack/legacy/plugins/spaces/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 2f3e5e0a86d21..79c57e564b4e1 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -17,7 +17,13 @@ export const spaces = (kibana: Record) => configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }) + .unknown() + .default(); + }, uiExports: { managementSections: [], apps: [], From 53b95424fec89bf503390bcafb5a62e04b28801b Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 1 Jun 2020 17:16:44 +0300 Subject: [PATCH 37/38] Deprecate es API exposed from setup contract (#67596) * move elasticsearch client under legacy namespace * update mocks and tests * update platform code * update legacy code * update plugins using elasticsearch setup API * update request handler context * update docs * rename remaining places * address comments * fix merge conflict error --- ...r.elasticsearchservicesetup.adminclient.md | 27 ----- ....elasticsearchservicesetup.createclient.md | 28 ------ ...er.elasticsearchservicesetup.dataclient.md | 27 ----- ...server.elasticsearchservicesetup.legacy.md | 19 ++++ ...n-core-server.elasticsearchservicesetup.md | 4 +- ...server.elasticsearchservicestart.legacy.md | 5 + .../core/server/kibana-plugin-core-server.md | 2 +- ...lugin-core-server.requesthandlercontext.md | 2 +- .../elasticsearch_service.mock.ts | 47 ++++----- .../elasticsearch_service.test.ts | 80 ++++++--------- .../elasticsearch/elasticsearch_service.ts | 74 +++++--------- src/core/server/elasticsearch/types.ts | 98 +++++++++---------- .../integration_tests/core_services.test.ts | 8 +- src/core/server/index.ts | 4 +- src/core/server/legacy/legacy_service.ts | 7 +- src/core/server/mocks.ts | 1 + src/core/server/plugins/plugin_context.ts | 4 +- .../saved_objects_service.test.ts | 47 +++++---- .../saved_objects/saved_objects_service.ts | 30 ++++-- src/core/server/server.api.md | 11 +-- src/core/server/server.ts | 3 +- .../core_plugins/elasticsearch/index.js | 8 +- .../saved_objects/saved_objects_mixin.test.js | 7 -- src/plugins/telemetry/server/plugin.ts | 2 +- .../csv/server/execute_job.test.ts | 6 +- .../export_types/csv/server/execute_job.ts | 2 +- .../server/lib/generate_csv_search.ts | 2 +- .../png/server/execute_job/index.test.ts | 6 +- .../server/execute_job/index.test.ts | 6 +- .../reporting/server/lib/create_queue.ts | 2 +- .../reporting/server/lib/jobs_query.ts | 2 +- .../validate_max_content_length.test.js | 32 +++--- .../validate/validate_max_content_length.ts | 2 +- .../server/routes/generation.test.ts | 4 +- .../reporting/server/routes/jobs.test.ts | 22 ++--- x-pack/plugins/licensing/server/plugin.ts | 8 +- x-pack/plugins/ml/server/plugin.ts | 2 +- .../server/kibana_monitoring/bulk_uploader.js | 6 +- x-pack/plugins/monitoring/server/plugin.ts | 4 +- .../server/routes/api/add_route.test.ts | 2 +- .../server/routes/api/delete_route.test.ts | 2 +- .../server/routes/api/get_route.test.ts | 2 +- .../server/routes/api/update_route.test.ts | 2 +- x-pack/plugins/security/server/plugin.test.ts | 8 +- x-pack/plugins/security/server/plugin.ts | 2 +- .../server/plugin.ts | 2 +- .../plugins/alerts/server/action_types.ts | 4 +- .../plugins/alerts/server/alert_types.ts | 4 +- .../sample_task_plugin/server/init_routes.ts | 4 +- .../sample_task_plugin/server/plugin.ts | 4 +- 50 files changed, 292 insertions(+), 395 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md deleted file mode 100644 index 3fcb855586129..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [adminClient](./kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md) - -## ElasticsearchServiceSetup.adminClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.client](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly adminClient: IClusterClient; -``` - -## Example - - -```js -const client = core.elasticsearch.adminClient; - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md deleted file mode 100644 index 75bf6c6aa461b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicesetup.createclient.md) - -## ElasticsearchServiceSetup.createClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.createClient](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; -``` - -## Example - - -```js -const client = elasticsearch.createCluster('my-app-name', config); -const data = await client.callAsInternalUser(); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md deleted file mode 100644 index 867cafa957f42..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [dataClient](./kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md) - -## ElasticsearchServiceSetup.dataClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.client](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly dataClient: IClusterClient; -``` - -## Example - - -```js -const client = core.elasticsearch.dataClient; - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md new file mode 100644 index 0000000000000..e8c4c63dc6a96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) + +## ElasticsearchServiceSetup.legacy property + +> Warning: This API is now obsolete. +> +> Use [ElasticsearchServiceStart.legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. +> + +Signature: + +```typescript +legacy: { + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; + readonly client: IClusterClient; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md index ee56f8b4a6284..c1e23527e9516 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md @@ -15,7 +15,5 @@ export interface ElasticsearchServiceSetup | Property | Type | Description | | --- | --- | --- | -| [adminClient](./kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md) | IClusterClient | | -| [createClient](./kibana-plugin-core-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | | -| [dataClient](./kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md) | IClusterClient | | +| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | {
    readonly createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient;
    readonly client: IClusterClient;
    } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md index 08765aaf93d3d..667a36091f232 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md @@ -4,6 +4,11 @@ ## ElasticsearchServiceStart.legacy property +> Warning: This API is now obsolete. +> +> Provided for the backward compatibility. Switch to the new elasticsearch client as soon as https://github.com/elastic/kibana/issues/35508 done. +> + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 14e01fda3d287..147a72016b235 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -125,7 +125,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 6b3fc4c03ec73..99be0676bcda3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index a7d78b56ff3fd..55e60f5987604 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -23,12 +23,7 @@ import { IClusterClient, ICustomClusterClient } from './cluster_client'; import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; -import { - InternalElasticsearchServiceSetup, - ElasticsearchServiceSetup, - ElasticsearchServiceStart, - ElasticsearchStatusMeta, -} from './types'; +import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; @@ -51,32 +46,26 @@ function createClusterClientMock() { return client; } -type MockedElasticSearchServiceSetup = jest.Mocked< - ElasticsearchServiceSetup & { - adminClient: jest.Mocked; - dataClient: jest.Mocked; - } ->; +interface MockedElasticSearchServiceSetup { + legacy: { + createClient: jest.Mock; + client: jest.Mocked; + }; +} const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { - createClient: jest.fn(), - adminClient: createClusterClientMock(), - dataClient: createClusterClientMock(), + legacy: { + createClient: jest.fn(), + client: createClusterClientMock(), + }, }; - setupContract.createClient.mockReturnValue(createCustomClusterClientMock()); - setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); - setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.createClient.mockReturnValue(createCustomClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; -type MockedElasticSearchServiceStart = { - legacy: jest.Mocked; -} & { - legacy: { - client: jest.Mocked; - }; -}; +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { @@ -92,13 +81,11 @@ const createStartContractMock = () => { type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { - adminClient: jest.Mocked; - dataClient: jest.Mocked; + legacy: { client: jest.Mocked }; } >; const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { - ...createSetupContractMock(), esNodesCompatibility$: new BehaviorSubject({ isCompatible: true, incompatibleNodes: [], @@ -111,10 +98,10 @@ const createInternalSetupContractMock = () => { }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), + ...createSetupContractMock().legacy, }, }; - setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); - setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 26144eaaa4afa..e7dab3807733a 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -74,25 +74,16 @@ describe('#setup', () => { ); }); - it('returns data and admin client as a part of the contract', async () => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + it('returns elasticsearch client as a part of the contract', async () => { + const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); const setupContract = await elasticsearchService.setup(deps); + const client = setupContract.legacy.client; - const adminClient = setupContract.adminClient; - const dataClient = setupContract.dataClient; - - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await adminClient.callAsInternalUser('any'); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); - - expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await dataClient.callAsInternalUser('any'); - expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + await client.callAsInternalUser('any'); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); }); describe('#createClient', () => { @@ -103,7 +94,7 @@ describe('#setup', () => { MockClusterClient.mockImplementation(() => mockClusterClientInstance); const customConfig = { logQueries: true }; - const clusterClient = setupContract.createClient('some-custom-type', customConfig); + const clusterClient = setupContract.legacy.createClient('some-custom-type', customConfig); expect(clusterClient).toBe(mockClusterClientInstance); @@ -124,7 +115,7 @@ describe('#setup', () => { logQueries: true, ssl: { certificate: 'certificate-value' }, }; - setupContract.createClient('some-custom-type', customConfig); + setupContract.legacy.createClient('some-custom-type', customConfig); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -149,7 +140,7 @@ describe('#setup', () => { // reset all mocks called during setup phase MockClusterClient.mockClear(); - setupContract.createClient('another-type'); + setupContract.legacy.createClient('another-type'); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -195,7 +186,7 @@ describe('#setup', () => { logQueries: true, ssl: { certificate: 'certificate-value' }, }; - setupContract.createClient('some-custom-type', customConfig); + setupContract.legacy.createClient('some-custom-type', customConfig); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -218,40 +209,34 @@ describe('#setup', () => { }); it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const clusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => clusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + clusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); await delay(10); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); setupContract.esNodesCompatibility$.subscribe(() => { - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async (done) => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); const sub = setupContract.esNodesCompatibility$.subscribe(async () => { sub.unsubscribe(); await delay(100); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); @@ -259,38 +244,31 @@ describe('#setup', () => { describe('#stop', () => { it('stops both admin and data clients', async () => { - const mockAdminClusterClientInstance = { close: jest.fn() }; - const mockDataClusterClientInstance = { close: jest.fn() }; - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const mockClusterClientInstance = { close: jest.fn() }; + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); await elasticsearchService.setup(deps); await elasticsearchService.stop(); - expect(mockAdminClusterClientInstance.close).toHaveBeenCalledTimes(1); - expect(mockDataClusterClientInstance.close).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.close).toHaveBeenCalledTimes(1); }); it('stops pollEsNodeVersions even if there are active subscriptions', async (done) => { expect.assertions(2); - const mockAdminClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); + const mockClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); setupContract.esNodesCompatibility$.subscribe(async () => { - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); await elasticsearchService.stop(); await delay(100); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index ab9c9e11fedc8..26001bf83924f 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -50,8 +50,7 @@ import { calculateStatus$ } from './status'; /** @internal */ interface CoreClusterClients { config: ElasticsearchConfig; - adminClient: ClusterClient; - dataClient: ClusterClient; + client: ClusterClient; } interface SetupDeps { @@ -70,7 +69,7 @@ export class ElasticsearchService type: string, clientConfig?: Partial ) => ICustomClusterClient; - private adminClient?: IClusterClient; + private client?: IClusterClient; constructor(private readonly coreContext: CoreContext) { this.kibanaVersion = coreContext.env.packageInfo.version; @@ -95,21 +94,19 @@ export class ElasticsearchService switchMap( (config) => new Observable((subscriber) => { - this.log.debug(`Creating elasticsearch clients`); + this.log.debug('Creating elasticsearch client'); const coreClients = { config, - adminClient: this.createClusterClient('admin', config), - dataClient: this.createClusterClient('data', config, deps.http.getAuthHeaders), + client: this.createClusterClient('data', config, deps.http.getAuthHeaders), }; subscriber.next(coreClients); return () => { - this.log.debug(`Closing elasticsearch clients`); + this.log.debug('Closing elasticsearch client'); - coreClients.adminClient.close(); - coreClients.dataClient.close(); + coreClients.client.close(); }; }) ), @@ -120,54 +117,27 @@ export class ElasticsearchService const config = await this.config$.pipe(first()).toPromise(); - const adminClient$ = clients$.pipe(map((clients) => clients.adminClient)); - const dataClient$ = clients$.pipe(map((clients) => clients.dataClient)); + const client$ = clients$.pipe(map((clients) => clients.client)); - this.adminClient = { + const client = { async callAsInternalUser( endpoint: string, clientParams: Record = {}, options?: CallAPIOptions ) { - const client = await adminClient$.pipe(take(1)).toPromise(); - return await client.callAsInternalUser(endpoint, clientParams, options); - }, - asScoped: (request: ScopeableRequest) => { - return { - callAsInternalUser: this.adminClient!.callAsInternalUser, - async callAsCurrentUser( - endpoint: string, - clientParams: Record = {}, - options?: CallAPIOptions - ) { - const client = await adminClient$.pipe(take(1)).toPromise(); - return await client - .asScoped(request) - .callAsCurrentUser(endpoint, clientParams, options); - }, - }; - }, - }; - - const dataClient = { - async callAsInternalUser( - endpoint: string, - clientParams: Record = {}, - options?: CallAPIOptions - ) { - const client = await dataClient$.pipe(take(1)).toPromise(); - return await client.callAsInternalUser(endpoint, clientParams, options); + const _client = await client$.pipe(take(1)).toPromise(); + return await _client.callAsInternalUser(endpoint, clientParams, options); }, asScoped(request: ScopeableRequest) { return { - callAsInternalUser: dataClient.callAsInternalUser, + callAsInternalUser: client.callAsInternalUser, async callAsCurrentUser( endpoint: string, clientParams: Record = {}, options?: CallAPIOptions ) { - const client = await dataClient$.pipe(take(1)).toPromise(); - return await client + const _client = await client$.pipe(take(1)).toPromise(); + return await _client .asScoped(request) .callAsCurrentUser(endpoint, clientParams, options); }, @@ -175,8 +145,10 @@ export class ElasticsearchService }, }; + this.client = client; + const esNodesCompatibility$ = pollEsNodesVersion({ - callWithInternalUser: this.adminClient.callAsInternalUser, + callWithInternalUser: client.callAsInternalUser, log: this.log, ignoreVersionMismatch: config.ignoreVersionMismatch, esVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), @@ -189,22 +161,22 @@ export class ElasticsearchService }; return { - legacy: { config$: clients$.pipe(map((clients) => clients.config)) }, + legacy: { + config$: clients$.pipe(map((clients) => clients.config)), + client, + createClient: this.createClient, + }, esNodesCompatibility$, - adminClient: this.adminClient, - dataClient, - createClient: this.createClient, status$: calculateStatus$(esNodesCompatibility$), }; } - public async start() { - if (typeof this.adminClient === 'undefined' || typeof this.createClient === 'undefined') { + if (typeof this.client === 'undefined' || typeof this.createClient === 'undefined') { throw new Error('ElasticsearchService needs to be setup before calling start'); } else { return { legacy: { - client: this.adminClient, + client: this.client, createClient: this.createClient, }, }; diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 3d38935e9fbf0..6fef08fc298ff 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -30,62 +30,60 @@ import { ServiceStatus } from '../status'; export interface ElasticsearchServiceSetup { /** * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. + * Use {@link ElasticsearchServiceStart.legacy} instead. * - * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createCluster('my-app-name', config); - * const data = await client.callAsInternalUser(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient; - - /** - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. - * See {@link IClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.adminClient; - * ``` - */ - readonly adminClient: IClusterClient; + * */ + legacy: { + /** + * @deprecated + * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. + * + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createCluster('my-app-name', config); + * const data = await client.callAsInternalUser(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; - /** - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. - * See {@link IClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.dataClient; - * ``` - */ - readonly dataClient: IClusterClient; + /** + * @deprecated + * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. + * + * All Elasticsearch config value changes are processed under the hood. + * See {@link IClusterClient}. + * + * @example + * ```js + * const client = core.elasticsearch.legacy.client; + * ``` + */ + readonly client: IClusterClient; + }; } /** * @public */ export interface ElasticsearchServiceStart { + /** + * @deprecated + * Provided for the backward compatibility. + * Switch to the new elasticsearch client as soon as https://github.com/elastic/kibana/issues/35508 done. + * */ legacy: { /** * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. @@ -123,9 +121,9 @@ export interface ElasticsearchServiceStart { } /** @internal */ -export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup { +export interface InternalElasticsearchServiceSetup { // Required for the BWC with the legacy Kibana only. - readonly legacy: { + readonly legacy: ElasticsearchServiceSetup['legacy'] & { readonly config$: Observable; }; esNodesCompatibility$: Observable; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 9583cca177619..ba39effa77016 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -383,8 +383,8 @@ describe('http service', () => { // client contains authHeaders for BWC with legacy platform. const [client] = clusterClientMock.mock.calls; - const [, , dataClientHeaders] = client; - expect(dataClientHeaders).toEqual(authHeaders); + const [, , clientHeaders] = client; + expect(clientHeaders).toEqual(authHeaders); }); it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { @@ -407,8 +407,8 @@ describe('http service', () => { .expect(200); const [client] = clusterClientMock.mock.calls; - const [, , dataClientHeaders] = client; - expect(dataClientHeaders).toEqual({ authorization: authorizationHeader }); + const [, , clientHeaders] = client; + expect(clientHeaders).toEqual({ authorization: authorizationHeader }); }); }); }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 96bb0c9a006b0..658c24f835020 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -338,10 +338,8 @@ export { * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing * all the registered types. - * - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch + * - {@link ScopedClusterClient | elasticsearch.legacy.client} - Elasticsearch * data client which uses the credentials of the incoming request - * - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch - * admin client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request * diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index cadbecb2e9a3f..2ced8b4762406 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -279,9 +279,10 @@ export class LegacyService implements CoreService { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, elasticsearch: { - adminClient: setupDeps.core.elasticsearch.adminClient, - dataClient: setupDeps.core.elasticsearch.dataClient, - createClient: setupDeps.core.elasticsearch.createClient, + legacy: { + client: setupDeps.core.elasticsearch.legacy.client, + createClient: setupDeps.core.elasticsearch.legacy.createClient, + }, }, http: { createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index f0fd471abb9be..b6e9ffef6f3f1 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -101,6 +101,7 @@ function pluginInitializerContextMock(config: T = {} as T) { } type CoreSetupMockType = MockedKeys & { + elasticsearch: ReturnType; getStartServices: jest.MockedFunction>; }; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index ab18a9cbbc062..7afb607192cae 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -147,9 +147,7 @@ export function createPluginSetupContext( createContextContainer: deps.context.createContextContainer, }, elasticsearch: { - adminClient: deps.elasticsearch.adminClient, - dataClient: deps.elasticsearch.dataClient, - createClient: deps.elasticsearch.createClient, + legacy: deps.elasticsearch.legacy, }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index bff392e8761eb..9fba2728003d2 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -67,6 +67,13 @@ describe('SavedObjectsService', () => { }; }; + const createStartDeps = (pluginsInitialized: boolean = true) => { + return { + pluginsInitialized, + elasticsearch: elasticsearchServiceMock.createStart(), + }; + }; + afterEach(() => { jest.clearAllMocks(); }); @@ -83,7 +90,7 @@ describe('SavedObjectsService', () => { setup.setClientFactoryProvider(factoryProvider); - await soService.start({}); + await soService.start(createStartDeps()); expect(clientProviderInstanceMock.setClientFactory).toHaveBeenCalledWith(factory); }); @@ -117,7 +124,7 @@ describe('SavedObjectsService', () => { setup.addClientWrapper(1, 'A', wrapperA); setup.addClientWrapper(2, 'B', wrapperB); - await soService.start({}); + await soService.start(createStartDeps()); expect(clientProviderInstanceMock.addClientWrapperFactory).toHaveBeenCalledTimes(2); expect(clientProviderInstanceMock.addClientWrapperFactory).toHaveBeenCalledWith( @@ -159,9 +166,10 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); + const coreStart = createStartDeps(); let i = 0; - coreSetup.elasticsearch.adminClient.callAsInternalUser = jest + coreStart.elasticsearch.legacy.client.callAsInternalUser = jest .fn() .mockImplementation(() => i++ <= 2 @@ -170,7 +178,7 @@ describe('SavedObjectsService', () => { ); await soService.setup(coreSetup); - await soService.start({}, 1); + await soService.start(coreStart, 1); return expect(KibanaMigratorMock.mock.calls[0][0].callCluster()).resolves.toMatch('success'); }); @@ -180,7 +188,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - await soService.start({ pluginsInitialized: false }); + await soService.start(createStartDeps(false)); expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); @@ -188,7 +196,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: true }); const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - await soService.start({}); + await soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); @@ -206,7 +214,7 @@ describe('SavedObjectsService', () => { kibanaVersion: '8.0.0', }); await soService.setup(setupDeps); - soService.start({}); + soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< NodesVersionCompatibility @@ -228,7 +236,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); - const startContract = await soService.start({}); + const startContract = await soService.start(createStartDeps()); expect(startContract.migrator).toBe(migratorInstanceMock); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); @@ -237,7 +245,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: false }); const soService = new SavedObjectsService(coreContext); const setup = await soService.setup(createSetupDeps()); - await soService.start({}); + await soService.start(createStartDeps()); expect(() => { setup.setClientFactoryProvider(jest.fn()); @@ -268,7 +276,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: false }); const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - const { getTypeRegistry } = await soService.start({}); + const { getTypeRegistry } = await soService.start(createStartDeps()); expect(getTypeRegistry()).toBe(typeRegistryInstanceMock); }); @@ -280,18 +288,19 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createScopedRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createScopedRepository } = await soService.start(coreStart); const req = {} as KibanaRequest; createScopedRepository(req); - expect(coreSetup.elasticsearch.adminClient.asScoped).toHaveBeenCalledWith(req); + expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledWith(req); const [ { value: { callAsCurrentUser }, }, - ] = coreSetup.elasticsearch.adminClient.asScoped.mock.results; + ] = coreStart.elasticsearch.legacy.client.asScoped.mock.results; const [ [, , , callCluster, includedHiddenTypes], @@ -306,7 +315,8 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createScopedRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createScopedRepository } = await soService.start(coreStart); const req = {} as KibanaRequest; createScopedRepository(req, ['someHiddenType']); @@ -325,7 +335,8 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createInternalRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createInternalRepository } = await soService.start(coreStart); createInternalRepository(); @@ -333,8 +344,8 @@ describe('SavedObjectsService', () => { [, , , callCluster, includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; - expect(coreSetup.elasticsearch.adminClient.callAsInternalUser).toBe(callCluster); - expect(callCluster).toBe(coreSetup.elasticsearch.adminClient.callAsInternalUser); + expect(coreStart.elasticsearch.legacy.client.callAsInternalUser).toBe(callCluster); + expect(callCluster).toBe(coreStart.elasticsearch.legacy.client.callAsInternalUser); expect(includedHiddenTypes).toEqual([]); }); @@ -343,7 +354,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createInternalRepository } = await soService.start({}); + const { createInternalRepository } = await soService.start(createStartDeps()); createInternalRepository(['someHiddenType']); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index a822a92acb91a..48b1e12fc187e 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -29,7 +29,12 @@ import { import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; import { LegacyServiceDiscoverPlugins } from '../legacy'; -import { InternalElasticsearchServiceSetup, APICaller } from '../elasticsearch'; +import { + APICaller, + ElasticsearchServiceStart, + IClusterClient, + InternalElasticsearchServiceSetup, +} from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { @@ -278,8 +283,8 @@ interface WrappedClientFactoryWrapper { } /** @internal */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsStartDeps { + elasticsearch: ElasticsearchServiceStart; pluginsInitialized?: boolean; } @@ -365,7 +370,7 @@ export class SavedObjectsService } public async start( - { pluginsInitialized = true }: SavedObjectsStartDeps, + { elasticsearch, pluginsInitialized = true }: SavedObjectsStartDeps, migrationsRetryDelay?: number ): Promise { if (!this.setupDeps || !this.config) { @@ -378,8 +383,14 @@ export class SavedObjectsService .atPath('kibana') .pipe(first()) .toPromise(); - const adminClient = this.setupDeps!.elasticsearch.adminClient; - const migrator = this.createMigrator(kibanaConfig, this.config.migration, migrationsRetryDelay); + const client = elasticsearch.legacy.client; + + const migrator = this.createMigrator( + kibanaConfig, + this.config.migration, + client, + migrationsRetryDelay + ); this.migrator$.next(migrator); @@ -435,9 +446,9 @@ export class SavedObjectsService const repositoryFactory: SavedObjectsRepositoryFactory = { createInternalRepository: (includedHiddenTypes?: string[]) => - createRepository(adminClient.callAsInternalUser, includedHiddenTypes), + createRepository(client.callAsInternalUser, includedHiddenTypes), createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => - createRepository(adminClient.asScoped(req).callAsCurrentUser, includedHiddenTypes), + createRepository(client.asScoped(req).callAsCurrentUser, includedHiddenTypes), }; const clientProvider = new SavedObjectsClientProvider({ @@ -473,10 +484,9 @@ export class SavedObjectsService private createMigrator( kibanaConfig: KibanaConfigType, savedObjectsConfig: SavedObjectsMigrationConfigType, + esClient: IClusterClient, migrationsRetryDelay?: number ): KibanaMigrator { - const adminClient = this.setupDeps!.elasticsearch.adminClient; - return new KibanaMigrator({ typeRegistry: this.typeRegistry, logger: this.logger, @@ -485,7 +495,7 @@ export class SavedObjectsService savedObjectValidations: this.validations, kibanaConfig, callCluster: migrationsRetryCallCluster( - adminClient.callAsInternalUser, + esClient.callAsInternalUser, this.logger, migrationsRetryDelay ), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 858458bfe40de..eef071e9488bf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -823,16 +823,15 @@ export class ElasticsearchErrorHelpers { // @public (undocumented) export interface ElasticsearchServiceSetup { // @deprecated (undocumented) - readonly adminClient: IClusterClient; - // @deprecated (undocumented) - readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; - // @deprecated (undocumented) - readonly dataClient: IClusterClient; + legacy: { + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; + readonly client: IClusterClient; + }; } // @public (undocumented) export interface ElasticsearchServiceStart { - // (undocumented) + // @deprecated (undocumented) legacy: { readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; readonly client: IClusterClient; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index b33528e2a3931..ef12379c199e8 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -195,12 +195,13 @@ export class Server { public async start() { this.log.debug('starting server'); + const elasticsearchStart = await this.elasticsearch.start(); const savedObjectsStart = await this.savedObjects.start({ + elasticsearch: elasticsearchStart, pluginsInitialized: this.pluginsInitialized, }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); - const elasticsearchStart = await this.elasticsearch.start(); this.coreStart = { capabilities: capabilitiesStart, diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index a7d6810ac6158..eb502e97fb77c 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -34,9 +34,9 @@ export default function (kibana) { // All methods that ES plugin exposes are synchronous so we should get the first // value from all observables here to be able to synchronously return and create // cluster clients afterwards. - const { adminClient, dataClient } = server.newPlatform.setup.core.elasticsearch; - const adminCluster = new Cluster(adminClient); - const dataCluster = new Cluster(dataClient); + const { client } = server.newPlatform.setup.core.elasticsearch.legacy; + const adminCluster = new Cluster(client); + const dataCluster = new Cluster(client); const esConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$ .pipe(first()) @@ -72,7 +72,7 @@ export default function (kibana) { } const cluster = new Cluster( - server.newPlatform.setup.core.elasticsearch.createClient(name, clientConfig) + server.newPlatform.setup.core.elasticsearch.legacy.createClient(name, clientConfig) ); clusters.set(name, cluster); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 5b40cc4b5aa35..63e4a632ab5e0 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -129,13 +129,6 @@ describe('Saved Objects Mixin', () => { waitUntilReady: jest.fn(), }, }, - newPlatform: { - __internals: { - elasticsearch: { - adminClient: { callAsInternalUser: mockCallCluster }, - }, - }, - }, }; const coreStart = coreMock.createStart(); diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 8ae63682413e5..e555c40d25592 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -82,7 +82,7 @@ export class TelemetryPlugin implements Plugin { const config$ = this.config$; const isDev = this.isDev; - registerCollection(telemetryCollectionManager, elasticsearch.dataClient); + registerCollection(telemetryCollectionManager, elasticsearch.legacy.client); const router = http.createRouter(); registerRoutes({ diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts index f6ae8edb9d5cb..dd7b37d989acb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts @@ -50,8 +50,10 @@ describe('CSV Execute Job', function () { let cancellationToken: any; const mockElasticsearch = { - dataClient: { - asScoped: () => clusterStub, + legacy: { + client: { + asScoped: () => clusterStub, + }, }, }; const mockUiSettingsClient = { diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 7d95c45d5d233..a6b2b0d0561d0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -81,7 +81,7 @@ export const executeJobFactory: ExecuteJobFactory callAsCurrentUser(endpoint, clientParams, options); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 51e0ddad53355..848623ede5b2f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -149,7 +149,7 @@ export async function generateCsvSearch( const config = reporting.getConfig(); const elasticsearch = await reporting.getElasticsearchService(); - const { callAsCurrentUser } = elasticsearch.dataClient.asScoped(req); + const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts index 72695545e3b5f..7e816e11f1953 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts @@ -59,8 +59,10 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, }, }; const mockGetElasticsearch = jest.fn(); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts index b081521fef8dd..fdf9c24730638 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts @@ -57,8 +57,10 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, }, }; const mockGetElasticsearch = jest.fn(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 2cac4bd654487..d993a17c0b314 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -52,7 +52,7 @@ export async function createQueueFactory( interval: queueIndexInterval, timeout: queueTimeout, dateSeparator: '.', - client: elasticsearch.dataClient, + client: elasticsearch.legacy.client, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 5153fd0f4e5b8..06c4a7714099e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -47,7 +47,7 @@ export function jobsQueryFactory( elasticsearch: ElasticsearchServiceSetup ) { const index = config.get('index'); - const { callAsInternalUser } = elasticsearch.adminClient; + const { callAsInternalUser } = elasticsearch.legacy.client; function execQuery(queryType: string, body: QueryBody) { const defaultBody: Record = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 2551fd48b91f3..f358021560cff 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -12,14 +12,16 @@ const ONE_HUNDRED_MEGABYTES = 104857600; describe('Reporting: Validate Max Content Length', () => { const elasticsearch = { - dataClient: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', + legacy: { + client: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, }, - }, - }), + }), + }, }, }; @@ -34,14 +36,16 @@ describe('Reporting: Validate Max Content Length', () => { it('should log warning messages when reporting has a higher max-size than elasticsearch', async () => { const config = { get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES) }; const elasticsearch = { - dataClient: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', + legacy: { + client: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, }, - }, - }), + }), + }, }, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index f6acf72612e01..6d34937d9bd75 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -18,7 +18,7 @@ export async function validateMaxContentLength( elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) { - const { callAsInternalUser } = elasticsearch.dataClient; + const { callAsInternalUser } = elasticsearch.legacy.client; const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { includeDefaults: true, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts index fdde3253cf28e..87ac71e250d0c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -51,7 +51,9 @@ describe('POST /api/reporting/generate', () => { ({ server, httpSetup } = await setupServer()); const mockDeps = ({ elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, + legacy: { + client: { callAsInternalUser: jest.fn() }, + }, }, security: { authc: { diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts index 73f3c660141c1..0911f48f82ca4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts @@ -43,7 +43,7 @@ describe('GET /api/reporting/jobs/download', () => { ({ server, httpSetup } = await setupServer()); core = await createMockReportingCore(config, ({ elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, + legacy: { client: { callAsInternalUser: jest.fn() } }, }, security: { authc: { @@ -89,7 +89,7 @@ describe('GET /api/reporting/jobs/download', () => { it('fails on malformed download IDs', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; registerJobInfoRoutes(core); @@ -158,7 +158,7 @@ describe('GET /api/reporting/jobs/download', () => { it('returns 404 if job not found', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; @@ -171,7 +171,7 @@ describe('GET /api/reporting/jobs/download', () => { it('returns a 401 if not a valid job type', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest .fn() .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), @@ -185,7 +185,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a job is incomplete', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest .fn() .mockReturnValue( @@ -205,7 +205,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a job fails', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue( Promise.resolve( getHits({ @@ -248,7 +248,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a known job-type is complete', async () => { const hits = getCompleteHits(); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); @@ -264,7 +264,7 @@ describe('GET /api/reporting/jobs/download', () => { it('succeeds when security is not there or disabled', async () => { const hits = getCompleteHits(); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; @@ -288,7 +288,7 @@ describe('GET /api/reporting/jobs/download', () => { outputContent: 'test', }); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); @@ -308,7 +308,7 @@ describe('GET /api/reporting/jobs/download', () => { outputContentType: 'application/pdf', }); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); @@ -329,7 +329,7 @@ describe('GET /api/reporting/jobs/download', () => { outputContentType: 'application/html', }); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 33f70c549914d..e1aa4a1b32517 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -107,7 +107,7 @@ export class LicensingPlugin implements Plugin ): ReturnType { const [coreStart] = await core.getStartServices(); - const client = coreStart.elasticsearch.legacy.client; - return await client.asScoped(request).callAsCurrentUser(...args); + const _client = coreStart.elasticsearch.legacy.client; + return await _client.asScoped(request).callAsCurrentUser(...args); }, callAsInternalUser, }; @@ -124,7 +124,7 @@ export class LicensingPlugin implements Plugin { + this._cluster = elasticsearch.legacy.createClient('monitoring-direct', config.elasticsearch); + elasticsearch.legacy.client.callAsInternalUser('info').then((data) => { this._productionClusterUuid = get(data, 'cluster_uuid'); }); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index a45e80ac71d65..14bef307b2f85 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -131,7 +131,7 @@ export class Plugin { this.legacyShimDependencies = { router: core.http.createRouter(), instanceUuid: core.uuid.getInstanceUuid(), - esDataClient: core.elasticsearch.dataClient, + esDataClient: core.elasticsearch.legacy.client, kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( KIBANA_STATS_TYPE_MONITORING ), @@ -142,7 +142,7 @@ export class Plugin { const cluster = (this.cluster = instantiateClient( config.ui.elasticsearch, this.log, - core.elasticsearch.createClient + core.elasticsearch.legacy.createClient )); // Start our license service which will ensure diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index cbfbbfd2d61f3..d28e95834ca0b 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -28,7 +28,7 @@ describe('ADD remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, payload }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index b9328cb61e967..d1e3cf89e94d9 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -30,7 +30,7 @@ describe('DELETE remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, params }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index f0444162e80b9..24e469c9ec9b2 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -29,7 +29,7 @@ describe('GET remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 7f8acb2b018d9..9669c98e1349e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -37,7 +37,7 @@ describe('UPDATE remote clusters', () => { }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index fc49bdd9bc0c3..3e30ff9447f3e 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -39,9 +39,7 @@ describe('Security Plugin', () => { mockCoreSetup.http.isTlsEnabled = true; mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); - mockCoreSetup.elasticsearch.createClient.mockReturnValue( - (mockClusterClient as unknown) as jest.Mocked - ); + mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; }); @@ -114,8 +112,8 @@ describe('Security Plugin', () => { it('properly creates cluster client instance', async () => { await plugin.setup(mockCoreSetup, mockDependencies); - expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledTimes(1); - expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledWith('security', { + expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledTimes(1); + expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledWith('security', { plugins: [elasticsearchClientPlugin], }); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index cdfc6f0ae542f..bdda0be9b15a7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -109,7 +109,7 @@ export class Plugin { .pipe(first()) .toPromise(); - this.clusterClient = core.elasticsearch.createClient('security', { + this.clusterClient = core.elasticsearch.legacy.createClient('security', { plugins: [elasticsearchClientPlugin], }); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index b0afba8852495..3f01d7423eded 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -18,7 +18,7 @@ export class TelemetryCollectionXpackPlugin implements Plugin { public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { telemetryCollectionManager.setCollection({ - esCluster: core.elasticsearch.dataClient, + esCluster: core.elasticsearch.legacy.client, title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index a921dac7d43a1..3b6befb3fe807 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server'; @@ -13,7 +13,7 @@ export function defineActionTypes( core: CoreSetup, { actions }: Pick ) { - const clusterClient = core.elasticsearch.adminClient; + const clusterClient = core.elasticsearch.legacy.client; // Action types const noopActionType: ActionType = { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index ff3a3e48d5b1a..bfabbb8169391 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { times } from 'lodash'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; @@ -14,7 +14,7 @@ export function defineAlertTypes( core: CoreSetup, { alerting }: Pick ) { - const clusterClient = core.elasticsearch.adminClient; + const clusterClient = core.elasticsearch.legacy.client; const alwaysFiringAlertType: AlertType = { id: 'test.always-firing', name: 'Test: Always Firing', diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index e4c4d13ee4a41..f35d6baac8f5a 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -11,7 +11,7 @@ import { IKibanaResponse, IRouter, CoreSetup, -} from 'kibana/server'; +} from 'src/core/server'; import { EventEmitter } from 'events'; import { TaskManagerStartContract } from '../../../../../plugins/task_manager/server'; @@ -39,7 +39,7 @@ export function initRoutes( taskTestingEvents: EventEmitter ) { async function ensureIndexIsRefreshed() { - return await core.elasticsearch.adminClient.callAsInternalUser('indices.refresh', { + return await core.elasticsearch.legacy.client.callAsInternalUser('indices.refresh', { index: '.kibana_task_manager', }); } diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts index ae756bb56b921..3ea669ae9d404 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; import { EventEmitter } from 'events'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; @@ -64,7 +64,7 @@ export class SampleTaskManagerFixturePlugin } } - await core.elasticsearch.adminClient.callAsInternalUser('index', { + await core.elasticsearch.legacy.client.callAsInternalUser('index', { index: '.kibana_task_manager_test_result', body: { type: 'task', From cf2aebf67a4280b94ec25371ba902314e580ec28 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 1 Jun 2020 11:02:46 -0400 Subject: [PATCH 38/38] [Ingest Manager] Optimize installation of integration (#67708) * call getArchiveInfo once first, pass paths to template * pass paths to installPreBuiltTemplates * pass paths to installILMPolicy * pass paths to ingest pipeline creation * use correct package key for cache * pass paths to kibana assets * cache other installed packages * create function for ensuring packages are cached * remove unused imports Co-authored-by: Elastic Machine --- .../services/epm/elasticsearch/ilm/install.ts | 14 ++------ .../elasticsearch/ingest_pipeline/install.ts | 22 +++++++------ .../epm/elasticsearch/template/install.ts | 32 ++++++------------- .../epm/kibana/index_pattern/install.ts | 8 +++++ .../server/services/epm/packages/assets.ts | 6 ++-- .../server/services/epm/packages/install.ts | 28 ++++++++-------- .../server/services/epm/registry/cache.ts | 1 + .../server/services/epm/registry/index.ts | 11 +++++-- 8 files changed, 60 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts index 1d06bf23a8c0f..9590167657d98 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts @@ -7,16 +7,8 @@ import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; import * as Registry from '../../registry'; -export async function installILMPolicy( - pkgName: string, - pkgVersion: string, - callCluster: CallESAsCurrentUser -) { - const ilmPaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isILMPolicy(entry) - ); +export async function installILMPolicy(paths: string[], callCluster: CallESAsCurrentUser) { + const ilmPaths = paths.filter((path) => isILMPolicy(path)); if (!ilmPaths.length) return; await Promise.all( ilmPaths.map(async (path) => { @@ -36,7 +28,7 @@ export async function installILMPolicy( }) ); } -const isILMPolicy = ({ path }: Registry.ArchiveEntry) => { +const isILMPolicy = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.ilmPolicy; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index bdf6ecfcdb9aa..11543fe73886f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -22,9 +22,11 @@ interface RewriteSubstitution { export const installPipelines = async ( registryPackage: RegistryPackage, + paths: string[], callCluster: CallESAsCurrentUser ) => { const datasets = registryPackage.datasets; + const pipelinePaths = paths.filter((path) => isPipeline(path)); if (datasets) { const pipelines = datasets.reduce>>((acc, dataset) => { if (dataset.ingest_pipeline) { @@ -32,7 +34,7 @@ export const installPipelines = async ( installPipelinesForDataset({ dataset, callCluster, - pkgName: registryPackage.name, + paths: pipelinePaths, pkgVersion: registryPackage.version, }) ); @@ -67,20 +69,16 @@ export function rewriteIngestPipeline( export async function installPipelinesForDataset({ callCluster, - pkgName, pkgVersion, + paths, dataset, }: { callCluster: CallESAsCurrentUser; - pkgName: string; pkgVersion: string; + paths: string[]; dataset: Dataset; }): Promise { - const pipelinePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isDatasetPipeline(entry, dataset.path) - ); + const pipelinePaths = paths.filter((path) => isDatasetPipeline(path, dataset.path)); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -152,8 +150,8 @@ async function installPipeline({ } const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/'); -const isDatasetPipeline = ({ path }: Registry.ArchiveEntry, datasetName: string) => { - // TODO: better way to get particular assets + +const isDatasetPipeline = (path: string, datasetName: string) => { const pathParts = Registry.pathParts(path); return ( !isDirectory({ path }) && @@ -162,6 +160,10 @@ const isDatasetPipeline = ({ path }: Registry.ArchiveEntry, datasetName: string) datasetName === pathParts.dataset ); }; +const isPipeline = (path: string) => { + const pathParts = Registry.pathParts(path); + return pathParts.type === ElasticsearchAssetType.ingestPipeline; +}; // XXX: assumes path/to/file.ext -- 0..n '/' and exactly one '.' const getNameAndExtension = ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index c600c8ba3efb8..9d0b6b5d078ad 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -16,13 +16,14 @@ export const installTemplates = async ( registryPackage: RegistryPackage, callCluster: CallESAsCurrentUser, pkgName: string, - pkgVersion: string + pkgVersion: string, + paths: string[] ): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates - await installPreBuiltComponentTemplates(pkgName, pkgVersion, callCluster); - await installPreBuiltTemplates(pkgName, pkgVersion, callCluster); + await installPreBuiltComponentTemplates(paths, callCluster); + await installPreBuiltTemplates(paths, callCluster); // build templates per dataset from yml files const datasets = registryPackage.datasets; @@ -44,16 +45,8 @@ export const installTemplates = async ( return []; }; -const installPreBuiltTemplates = async ( - pkgName: string, - pkgVersion: string, - callCluster: CallESAsCurrentUser -) => { - const templatePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isTemplate(entry) - ); +const installPreBuiltTemplates = async (paths: string[], callCluster: CallESAsCurrentUser) => { + const templatePaths = paths.filter((path) => isTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); @@ -95,15 +88,10 @@ const installPreBuiltTemplates = async ( }; const installPreBuiltComponentTemplates = async ( - pkgName: string, - pkgVersion: string, + paths: string[], callCluster: CallESAsCurrentUser ) => { - const templatePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isComponentTemplate(entry) - ); + const templatePaths = paths.filter((path) => isComponentTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); @@ -134,12 +122,12 @@ const installPreBuiltComponentTemplates = async ( } }; -const isTemplate = ({ path }: Registry.ArchiveEntry) => { +const isTemplate = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.indexTemplate; }; -const isComponentTemplate = ({ path }: Registry.ArchiveEntry) => { +const isComponentTemplate = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.componentTemplate; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index f321e2d614a04..0f7b1d6cab178 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -86,6 +86,14 @@ export async function installIndexPatterns( savedObjectsClient, InstallationStatus.installed ); + + // TODO: move to install package + // cache all installed packages if they don't exist + const packagePromises = installedPackages.map((pkg) => + Registry.ensureCachedArchiveInfo(pkg.pkgName, pkg.pkgVersion) + ); + await Promise.all(packagePromises); + if (pkgName && pkgVersion) { // add this package to the array if it doesn't already exist const foundPkg = installedPackages.find((pkg) => pkg.pkgName === pkgName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index c6f7a1f6b97aa..37fcf0db67131 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { RegistryPackage } from '../../../types'; import * as Registry from '../registry'; -import { cacheHas } from '../registry/cache'; +import { ensureCachedArchiveInfo } from '../registry'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml` @@ -57,8 +57,8 @@ export async function getAssetsData( datasetName?: string ): Promise { // TODO: Needs to be called to fill the cache but should not be required - const pkgkey = packageInfo.name + '-' + packageInfo.version; - if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(packageInfo.name, packageInfo.version); + + await ensureCachedArchiveInfo(packageInfo.name, packageInfo.version); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index dddb21bc4e075..7c0d5d571f6a5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -90,7 +90,7 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); - + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); // see if some version of this package is already installed // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets @@ -119,15 +119,16 @@ export async function installPackage(options: { savedObjectsClient, pkgName, pkgVersion, + paths, }), - installPipelines(registryPackageInfo, callCluster), + installPipelines(registryPackageInfo, paths, callCluster), // index patterns and ilm policies are not currently associated with a particular package // so we do not save them in the package saved object state. installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), // currenly only the base package has an ILM policy // at some point ILM policies can be installed/modified // per dataset and we should then save them - installILMPolicy(pkgName, pkgVersion, callCluster), + installILMPolicy(paths, callCluster), ]); // install or update the templates @@ -135,7 +136,8 @@ export async function installPackage(options: { registryPackageInfo, callCluster, pkgName, - pkgVersion + pkgVersion, + paths ); const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); @@ -186,13 +188,14 @@ export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; + paths: string[]; }) { - const { savedObjectsClient, pkgName, pkgVersion } = options; + const { savedObjectsClient, paths } = options; // Only install Kibana assets during package installation. const kibanaAssetTypes = Object.values(KibanaAssetType); const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, pkgName, pkgVersion, assetType }) + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) ); // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] @@ -237,19 +240,16 @@ export async function saveInstallationReferences(options: { async function installKibanaSavedObjects({ savedObjectsClient, - pkgName, - pkgVersion, assetType, + paths, }: { savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; assetType: KibanaAssetType; + paths: string[]; }) { - const isSameType = ({ path }: Registry.ArchiveEntry) => - assetType === Registry.pathParts(path).type; - const paths = await Registry.getArchiveInfo(pkgName, pkgVersion, isSameType); - const toBeSavedObjects = await Promise.all(paths.map(getObject)); + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); if (toBeSavedObjects.length === 0) { return []; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts index 17d52bc745a55..d2a14fcf04dff 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts @@ -8,3 +8,4 @@ const cache: Map = new Map(); export const cacheGet = (key: string) => cache.get(key); export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); export const cacheHas = (key: string) => cache.has(key); +export const getCacheKey = (key: string) => key + '.tar.gz'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 8e9b920875617..0393cabca8ba2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -16,7 +16,7 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { cacheGet, cacheSet } from './cache'; +import { cacheGet, cacheSet, getCacheKey, cacheHas } from './cache'; import { ArchiveEntry, untarBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; @@ -135,7 +135,7 @@ async function extract( async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { // assume .tar.gz for now. add support for .zip if/when we need it - const key = `${pkgName}-${pkgVersion}.tar.gz`; + const key = getCacheKey(`${pkgName}-${pkgVersion}`); let buffer = cacheGet(key); if (!buffer) { buffer = await fetchArchiveBuffer(pkgName, pkgVersion); @@ -149,6 +149,13 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro } } +export async function ensureCachedArchiveInfo(name: string, version: string) { + const pkgkey = getCacheKey(`${name}-${version}`); + if (!cacheHas(pkgkey)) { + await getArchiveInfo(name, version); + } +} + async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); const registryUrl = getRegistryUrl();