From 59affbaaf9c921ff5e269c99ec49aae120f8f92e Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 25 Nov 2019 11:27:54 +0100 Subject: [PATCH 01/63] [SIEM] Returns failure if some tests fails (#51439) * returns failure if some tests fails * Update x-pack/legacy/plugins/siem/package.json Co-Authored-By: Spencer --- x-pack/legacy/plugins/siem/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ca5fefe52bcc4..29c26c5f674e3 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -7,7 +7,7 @@ "scripts": { "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "../../../node_modules/.bin/cypress open", - "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/" + "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/ && exit $status;" }, "devDependencies": { "@types/lodash": "^4.14.110", From a1a256ddcbea6f120d06f29c603a96e4f5ea1689 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 25 Nov 2019 12:20:48 +0100 Subject: [PATCH 02/63] [ML] Reactive time-range selection in SMV (#51008) * [ML] http service to TS, add httpCall using fromFetch * [ML] types, add esSearchRx * [ML] timeresiesexplorer_contans to ts * [ML] timeseries_search_service to TS, add getMetricDataRx * [ML] result service with observables * [ML] update resolvers, forecast data support * [ML] wip timeseriesexplorer * [ML] fix state update for zoom * [ML] skip loading update * [ML] cleanup contextChartSelected * [ML] add to subscriptions * [ML] update imports * [ML] timeseriesexplorer_utils * [ML] refactor result service * [ML] getAnnotations * [ML] rename subject * [ML] fix explorer and unit tests * [ML] fix forecast * [ML] replace skipWhilte with filter * [ML] rename http$ * [ML] rename esSearch$ * [ML] remove filter operator, check for contextChartData before calculating the default range * [ML] remove casting for FocusData * [ML] replace with an arrow function * [ML] fix Job import path * [ML] fix annotations --- .../plugins/ml/common/util/job_utils.d.ts | 4 + .../annotations_table/annotations_table.js | 2 +- .../annotations_table.test.js | 16 +- .../explorer_charts_container_service.js | 8 +- .../explorer_charts_container_service.test.js | 55 +- .../application/explorer/explorer_utils.js | 4 +- .../common/results_loader/results_loader.ts | 20 +- .../services/forecast_service.d.ts | 26 + .../application/services/forecast_service.js | 186 +++--- .../application/services/http_service.js | 50 -- .../application/services/http_service.ts | 89 +++ .../{annotations.js => annotations.ts} | 27 +- .../services/ml_api_service/index.d.ts | 5 + .../services/ml_api_service/index.js | 9 +- .../services/ml_api_service/results.js | 8 +- .../application/services/results_service.d.ts | 61 -- .../services/results_service/index.ts | 55 ++ .../results_service/result_service_rx.ts | 534 +++++++++++++++++ .../results_service/results_service.d.ts | 37 ++ .../{ => results_service}/results_service.js | 564 +----------------- .../timeseries_chart/timeseries_chart.js | 4 - .../application/timeseriesexplorer/index.js | 6 +- ...ervice.js => timeseries_search_service.ts} | 92 +-- .../timeseriesexplorer/timeseriesexplorer.js | 329 ++++++---- ...nts.js => timeseriesexplorer_constants.ts} | 1 - .../get_focus_data.ts | 164 +++++ .../timeseriesexplorer_utils/index.ts | 8 + .../timeseriesexplorer_utils.d.ts | 52 ++ .../timeseriesexplorer_utils.js | 170 +----- 29 files changed, 1429 insertions(+), 1157 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/services/http_service.js create mode 100644 x-pack/legacy/plugins/ml/public/application/services/http_service.ts rename x-pack/legacy/plugins/ml/public/application/services/ml_api_service/{annotations.js => annotations.ts} (55%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts rename x-pack/legacy/plugins/ml/public/application/services/{ => results_service}/results_service.js (70%) rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/{timeseries_search_service.js => timeseries_search_service.ts} (69%) rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/{timeseriesexplorer_constants.js => timeseriesexplorer_constants.ts} (99%) create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/{ => timeseriesexplorer_utils}/timeseriesexplorer_utils.js (70%) diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts index 23152afe0af2f..df62d19b6d27b 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; + export interface ValidationMessage { id: string; } @@ -39,3 +41,5 @@ export function validateModelMemoryLimitUnits( export function processCreatedBy(customSettings: { created_by?: string }): void; export function mlFunctionToESAggregation(functionName: string): string | null; + +export function isModelPlotEnabled(job: Job, detectorIndex: number, entityFields: any[]): boolean; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 3e5afd3c1e7e7..909abfd4abc23 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -87,7 +87,7 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component { earliestMs: null, latestMs: null, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { + }).toPromise().then((resp) => { this.setState((prevState, props) => ({ annotations: resp.annotations[props.jobs[0].job_id] || [], errorMessage: undefined, diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 1ed30c7e13727..c3ca28dc96bfc 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -24,13 +24,17 @@ jest.mock('../../../services/job_service', () => ({ } })); -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - annotations: { - getAnnotations: jest.fn().mockResolvedValue({ annotations: [] }) +jest.mock('../../../services/ml_api_service', () => { + const { of } = require('rxjs'); + const mockAnnotations$ = of({ annotations: [] }); + return { + ml: { + annotations: { + getAnnotations: jest.fn().mockReturnValue(mockAnnotations$) + } } - } -})); + };} +); describe('AnnotationsTable', () => { test('Minimal initialization without props.', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index b222b6e1160c6..01afd9ffb602f 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -124,7 +124,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, config.interval - ); + ).toPromise(); } else { // Extract the partition, by, over fields on which to filter. const criteriaFields = []; @@ -169,7 +169,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, interval - ) + ).toPromise() .then((resp) => { // Return data in format required by the explorer charts. const results = resp.results; @@ -201,7 +201,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, ANOMALIES_MAX_RESULTS - ); + ).toPromise(); } // Query 3 - load any scheduled events for the job. @@ -213,7 +213,7 @@ export function explorerChartsContainerServiceFactory(callback) { config.interval, 1, MAX_SCHEDULED_EVENTS - ); + ).toPromise(); } // Query 4 - load context data distribution diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index b907cd92df10c..f8ed067a3de54 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -47,36 +47,39 @@ jest.mock('../../services/job_service', () => ({ } })); -jest.mock('../../services/results_service', () => ({ - mlResultsService: { - getMetricData(indices) { +jest.mock('../../services/results_service', () => { + const { of } = require('rxjs'); + return { + mlResultsService: { + getMetricData(indices) { // this is for 'call anomalyChangeListener with actual series config' - if (indices[0] === 'farequote-2017') { - return Promise.resolve(mockSeriesPromisesResponse[0][0]); - } - // this is for 'filtering should skip values of null' - return Promise.resolve(mockMetricClone); - }, - getRecordsForCriteria() { - return Promise.resolve(mockSeriesPromisesResponse[0][1]); - }, - getScheduledEventsByBucket() { - return Promise.resolve(mockSeriesPromisesResponse[0][2]); - }, - getEventDistributionData(indices) { + if (indices[0] === 'farequote-2017') { + return of(mockSeriesPromisesResponse[0][0]); + } + // this is for 'filtering should skip values of null' + return of(mockMetricClone); + }, + getRecordsForCriteria() { + return of(mockSeriesPromisesResponse[0][1]); + }, + getScheduledEventsByBucket() { + return of(mockSeriesPromisesResponse[0][2]); + }, + getEventDistributionData(indices) { // this is for 'call anomalyChangeListener with actual series config' - if (indices[0] === 'farequote-2017') { - return Promise.resolve([]); + if (indices[0] === 'farequote-2017') { + return Promise.resolve([]); + } + // this is for 'filtering should skip values of null' and + // resolves with a dummy object to trigger the processing + // of the event distribution chartdata filtering + return Promise.resolve([{ + entity: 'mock' + }]); } - // this is for 'filtering should skip values of null' and - // resolves with a dummy object to trigger the processing - // of the event distribution chartdata filtering - return Promise.resolve([{ - entity: 'mock' - }]); } - } -})); + }; +}); jest.mock('../../util/string_utils', () => ({ mlEscape(d) { return d; } diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 9c2d2041566e1..5ca8681d16749 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -417,7 +417,7 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { + }).toPromise().then((resp) => { if (resp.error !== undefined || resp.annotations === undefined) { return resolve([]); } @@ -477,7 +477,7 @@ export async function loadAnomaliesTableData( ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, MAX_CATEGORY_EXAMPLES, influencersFilterQuery - ).then((resp) => { + ).toPromise().then((resp) => { const anomalies = resp.anomalies; const detectorsByJob = mlJobService.detectorsByJob; anomalies.forEach((anomaly) => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index d434e1be42e66..82808ef3d37ee 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -150,15 +150,17 @@ export class ResultsLoader { if (agg === null) { return { [dtrIndex]: [emptyModelItem] }; } - const resp = await mlResultsService.getModelPlotOutput( - this._jobCreator.jobId, - dtrIndex, - [], - this._lastModelTimeStamp, - this._jobCreator.end, - `${this._chartInterval.getInterval().asMilliseconds()}ms`, - agg.mlModelPlotAgg - ); + const resp = await mlResultsService + .getModelPlotOutput( + this._jobCreator.jobId, + dtrIndex, + [], + this._lastModelTimeStamp, + this._jobCreator.end, + `${this._chartInterval.getInterval().asMilliseconds()}ms`, + agg.mlModelPlotAgg + ) + .toPromise(); return this._createModel(resp, dtrIndex); } diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts new file mode 100644 index 0000000000000..19f77d97a5708 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.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 { Observable } from 'rxjs'; +import { Job } from '../jobs/new_job/common/job_creator/configs'; + +export interface ForecastData { + success: boolean; + results: any; +} + +export const mlForecastService: { + getForecastData: ( + job: Job, + detectorIndex: number, + forecastId: string, + entityFields: any[], + earliestMs: number, + latestMs: number, + interval: string, + aggType: any + ) => Observable; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js index c420cca579c9c..4b6ce19b5e6c6 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js @@ -9,6 +9,7 @@ // Service for carrying out requests to run ML forecasts and to obtain // data on forecasts that have been performed. import _ from 'lodash'; +import { map } from 'rxjs/operators'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { ml } from './ml_api_service'; @@ -192,117 +193,112 @@ function getForecastData( } } - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID, forecast ID, detector index, result type and time range. - const filterCriteria = [{ - query_string: { - query: 'result_type:model_forecast', - analyze_wildcard: true - } - }, - { - term: { job_id: job.job_id } - }, - { - term: { forecast_id: forecastId } - }, - { - term: { detector_index: detectorIndex } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } + const obj = { + success: true, + results: {} + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, forecast ID, detector index, result type and time range. + const filterCriteria = [{ + query_string: { + query: 'result_type:model_forecast', + analyze_wildcard: true + } + }, + { + term: { job_id: job.job_id } + }, + { + term: { forecast_id: forecastId } + }, + { + term: { detector_index: detectorIndex } + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' } - }]; + } + }]; - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - filterCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, (criteria) => { + filterCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue + } }); + }); - // If an aggType object has been passed in, use it. - // Otherwise default to avg, min and max aggs for the - // forecast prediction, upper and lower - const forecastAggs = (aggType === undefined) ? - { avg: 'avg', max: 'max', min: 'min' } : - { - avg: aggType.avg, - max: aggType.max, - min: aggType.min - }; + // If an aggType object has been passed in, use it. + // Otherwise default to avg, min and max aggs for the + // forecast prediction, upper and lower + const forecastAggs = (aggType === undefined) ? + { avg: 'avg', max: 'max', min: 'min' } : + { + avg: aggType.avg, + max: aggType.max, + min: aggType.min + }; - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: filterCriteria - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 + return ml.esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: filterCriteria + } + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval: interval, + min_doc_count: 1 + }, + aggs: { + prediction: { + [forecastAggs.avg]: { + field: 'forecast_prediction' + } + }, + forecastUpper: { + [forecastAggs.max]: { + field: 'forecast_upper' + } }, - aggs: { - prediction: { - [forecastAggs.avg]: { - field: 'forecast_prediction' - } - }, - forecastUpper: { - [forecastAggs.max]: { - field: 'forecast_upper' - } - }, - forecastLower: { - [forecastAggs.min]: { - field: 'forecast_lower' - } + forecastLower: { + [forecastAggs.min]: { + field: 'forecast_lower' } } } } } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - obj.results[time] = { - prediction: _.get(dataForTime, ['prediction', 'value']), - forecastUpper: _.get(dataForTime, ['forecastUpper', 'value']), - forecastLower: _.get(dataForTime, ['forecastLower', 'value']) - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); + } + }).pipe( + map(resp => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime) => { + const time = dataForTime.key; + obj.results[time] = { + prediction: _.get(dataForTime, ['prediction', 'value']), + forecastUpper: _.get(dataForTime, ['forecastUpper', 'value']), + forecastLower: _.get(dataForTime, ['forecastLower', 'value']) + }; }); - }); + return obj; + }) + ); } // Runs a forecast diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.js b/x-pack/legacy/plugins/ml/public/application/services/http_service.js deleted file mode 100644 index f0bef4396e4f3..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/http_service.js +++ /dev/null @@ -1,50 +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. - */ - - - -// service for interacting with the server - -import chrome from 'ui/chrome'; - -import { addSystemApiHeader } from 'ui/system_api'; - -export function http(options) { - return new Promise((resolve, reject) => { - if(options && options.url) { - let url = ''; - url = url + (options.url || ''); - const headers = addSystemApiHeader({ - 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), - ...options.headers - }); - - const allHeaders = (options.headers === undefined) ? headers : { ...options.headers, ...headers }; - const body = (options.data === undefined) ? null : JSON.stringify(options.data); - - const payload = { - method: (options.method || 'GET'), - headers: allHeaders, - credentials: 'same-origin' - }; - - if (body !== null) { - payload.body = body; - } - - fetch(url, payload) - .then((resp) => { - resp.json().then((resp.ok === true) ? resolve : reject); - }) - .catch((resp) => { - reject(resp); - }); - } else { - reject(); - } - }); -} diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts new file mode 100644 index 0000000000000..1d68ec5b886eb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// service for interacting with the server + +import chrome from 'ui/chrome'; + +// @ts-ignore +import { addSystemApiHeader } from 'ui/system_api'; +import { fromFetch } from 'rxjs/fetch'; +import { from, Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +export interface HttpOptions { + url?: string; +} + +function getResultHeaders(headers: HeadersInit): HeadersInit { + return addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...headers, + }); +} + +export function http(options: any) { + return new Promise((resolve, reject) => { + if (options && options.url) { + let url = ''; + url = url + (options.url || ''); + const headers: Record = addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...options.headers, + }); + + const allHeaders = + options.headers === undefined ? headers : { ...options.headers, ...headers }; + const body = options.data === undefined ? null : JSON.stringify(options.data); + + const payload: RequestInit = { + method: options.method || 'GET', + headers: allHeaders, + credentials: 'same-origin', + }; + + if (body !== null) { + payload.body = body; + } + + fetch(url, payload) + .then(resp => { + resp.json().then(resp.ok === true ? resolve : reject); + }) + .catch(resp => { + reject(resp); + }); + } else { + reject(); + } + }); +} + +interface RequestOptions extends RequestInit { + body: BodyInit | any; +} + +export function http$(url: string, options: RequestOptions): Observable { + const requestInit: RequestInit = { + ...options, + credentials: 'same-origin', + method: options.method || 'GET', + ...(options.body ? { body: JSON.stringify(options.body) as string } : {}), + headers: getResultHeaders(options.headers ?? {}), + }; + + return fromFetch(url, requestInit).pipe( + switchMap(response => { + if (response.ok) { + return from(response.json() as Promise); + } else { + throw new Error(String(response.status)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts similarity index 55% rename from x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts index 560c4c460e118..54d55159646f6 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -4,33 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ - - import chrome from 'ui/chrome'; -import { http } from '../http_service'; +import { Annotation } from '../../../../common/types/annotations'; +import { http, http$ } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); export const annotations = { - getAnnotations(obj) { - return http({ - url: `${basePath}/annotations`, + getAnnotations(obj: { + jobIds: string[]; + earliestMs: number; + latestMs: number; + maxAnnotations: number; + }) { + return http$<{ annotations: Record }>(`${basePath}/annotations`, { method: 'POST', - data: obj + body: obj, }); }, - indexAnnotation(obj) { + indexAnnotation(obj: any) { return http({ url: `${basePath}/annotations/index`, method: 'PUT', - data: obj + data: obj, }); }, - deleteAnnotation(id) { + deleteAnnotation(id: string) { return http({ url: `${basePath}/annotations/delete/${id}`, - method: 'DELETE' + method: 'DELETE', }); - } + }, }; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index 11c65851270eb..7c0b22b0e1966 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Observable } from 'rxjs'; import { Annotation } from '../../../../common/types/annotations'; import { AggFieldNamePair } from '../../../../common/types/fields'; import { ExistingJobsAndGroups } from '../job_service'; @@ -15,6 +16,7 @@ import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analyt import { JobMessage } from '../../../../common/types/audit_message'; import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; import { DeepPartial } from '../../../../common/types/common'; +import { annotations } from './annotations'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -65,6 +67,7 @@ declare interface Ml { annotations: { deleteAnnotation(id: string | undefined): Promise; indexAnnotation(annotation: Annotation): Promise; + getAnnotations: typeof annotations.getAnnotations; }; dataFrameAnalytics: { @@ -92,6 +95,7 @@ declare interface Ml { getJobStats(obj: object): Promise; getDatafeedStats(obj: object): Promise; esSearch(obj: object): any; + esSearch$(obj: object): Observable; getIndices(): Promise; dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; getDataRecognizerModule(obj: { moduleId: string }): Promise; @@ -159,6 +163,7 @@ declare interface Ml { mlNodeCount(): Promise<{ count: number }>; mlInfo(): Promise; + getCardinalityOfFields(obj: Record): any; } declare const ml: Ml; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js index b3310eb6bcd53..34d9f9ec16f83 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -9,7 +9,7 @@ import { pick } from 'lodash'; import chrome from 'ui/chrome'; -import { http } from '../http_service'; +import { http, http$ } from '../http_service'; import { annotations } from './annotations'; import { dataFrameAnalytics } from './data_frame_analytics'; @@ -444,6 +444,13 @@ export const ml = { }); }, + esSearch$(obj) { + return http$(`${basePath}/es_search`, { + method: 'POST', + body: obj + }); + }, + getIndices() { const tempBasePath = chrome.addBasePath('/api'); return http({ diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index 4bfec7643cecc..f9874cca840a7 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../http_service'; +import { http, http$ } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); @@ -25,11 +25,9 @@ export const results = { maxRecords, maxExamples, influencersFilterQuery) { - - return http({ - url: `${basePath}/results/anomalies_table_data`, + return http$(`${basePath}/results/anomalies_table_data`, { method: 'POST', - data: { + body: { jobIds, criteriaFields, influencers, diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts deleted file mode 100644 index 2bbe37c3fc05d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts +++ /dev/null @@ -1,61 +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. - */ - -type time = string; -export interface ModelPlotOutputResults { - results: Record; -} - -declare interface MlResultsService { - getScoresByBucket: ( - jobIds: string[], - earliestMs: number, - latestMs: number, - interval: string | number, - maxResults: number - ) => Promise; - getScheduledEventsByBucket: () => Promise; - getTopInfluencers: () => Promise; - getTopInfluencerValues: () => Promise; - getOverallBucketScores: ( - jobIds: any, - topN: any, - earliestMs: any, - latestMs: any, - interval?: any - ) => Promise; - getInfluencerValueMaxScoreByTime: () => Promise; - getRecordInfluencers: () => Promise; - getRecordsForInfluencer: () => Promise; - getRecordsForDetector: () => Promise; - getRecords: () => Promise; - getRecordsForCriteria: () => Promise; - getMetricData: () => Promise; - getEventRateData: ( - index: string, - query: any, - timeFieldName: string, - earliestMs: number, - latestMs: number, - interval: string | number - ) => Promise; - getEventDistributionData: () => Promise; - getModelPlotOutput: ( - jobId: string, - detectorIndex: number, - criteriaFields: string[], - earliestMs: number, - latestMs: number, - interval: string | number, - aggType: { - min: string; - max: string; - } - ) => Promise; - getRecordMaxScoreByTime: () => Promise; -} - -export const mlResultsService: MlResultsService; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts new file mode 100644 index 0000000000000..9ab14aa7495a7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getMetricData, + getModelPlotOutput, + getRecordsForCriteria, + getScheduledEventsByBucket, +} from './result_service_rx'; +import { + getEventDistributionData, + getEventRateData, + getInfluencerValueMaxScoreByTime, + getOverallBucketScores, + getRecordInfluencers, + getRecordMaxScoreByTime, + getRecords, + getRecordsForDetector, + getRecordsForInfluencer, + getScoresByBucket, + getTopInfluencers, + getTopInfluencerValues, +} from './results_service'; + +export const mlResultsService = { + getScoresByBucket, + getScheduledEventsByBucket, + getTopInfluencers, + getTopInfluencerValues, + getOverallBucketScores, + getInfluencerValueMaxScoreByTime, + getRecordInfluencers, + getRecordsForInfluencer, + getRecordsForDetector, + getRecords, + getRecordsForCriteria, + getMetricData, + getEventRateData, + getEventDistributionData, + getModelPlotOutput, + getRecordMaxScoreByTime, +}; + +type time = string; +export interface ModelPlotOutputResults { + results: Record; +} + +export interface CriteriaField { + fieldName: string; + fieldValue: any; +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts new file mode 100644 index 0000000000000..2341ae15a3378 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -0,0 +1,534 @@ +/* + * 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. + */ + +// Queries Elasticsearch to obtain metric aggregation results. +// index can be a String, or String[], of index names to search. +// entityFields parameter must be an array, with each object in the array having 'fieldName' +// and 'fieldValue' properties. +// Extra query object can be supplied, or pass null if no additional query +// to that built from the supplied entity fields. +// Returned response contains a results property containing the requested aggregation. +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import _ from 'lodash'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { ml } from '../ml_api_service'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; +import { CriteriaField } from './index'; + +interface ResultResponse { + success: boolean; +} + +export interface MetricData extends ResultResponse { + results: Record; +} + +export function getMetricData( + index: string, + entityFields: any[], + query: object | undefined, + metricFunction: string, // ES aggregation name + metricFieldName: string, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string +): Observable { + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, entity fields, + // plus any additional supplied query. + const shouldCriteria: object[] = []; + const mustCriteria: object[] = [ + { + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ...(query ? [query] : []), + ]; + + entityFields.forEach(entity => { + if (entity.fieldValue.length !== 0) { + mustCriteria.push({ + term: { + [entity.fieldName]: entity.fieldValue, + }, + }); + } else { + // Add special handling for blank entity field values, checking for either + // an empty string or the field not existing. + shouldCriteria.push({ + bool: { + must: [ + { + term: { + [entity.fieldName]: '', + }, + }, + ], + }, + }); + shouldCriteria.push({ + bool: { + must_not: [ + { + exists: { field: entity.fieldName }, + }, + ], + }, + }); + } + }); + + const body: any = { + query: { + bool: { + must: mustCriteria, + }, + }, + size: 0, + _source: { + excludes: [], + }, + aggs: { + byTime: { + date_histogram: { + field: timeFieldName, + interval, + min_doc_count: 0, + }, + }, + }, + }; + + if (shouldCriteria.length > 0) { + body.query.bool.should = shouldCriteria; + body.query.bool.minimum_should_match = shouldCriteria.length / 2; + } + + if (metricFieldName !== undefined && metricFieldName !== '') { + body.aggs.byTime.aggs = {}; + + const metricAgg: any = { + [metricFunction]: { + field: metricFieldName, + }, + }; + + if (metricFunction === 'percentiles') { + metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; + } + body.aggs.byTime.aggs.metric = metricAgg; + } + + return ml.esSearch$({ index, body }).pipe( + map((resp: any) => { + const obj: MetricData = { success: true, results: {} }; + const dataByTime = resp?.aggregations?.byTime?.buckets ?? []; + dataByTime.forEach((dataForTime: any) => { + if (metricFunction === 'count') { + obj.results[dataForTime.key] = dataForTime.doc_count; + } else { + const value = dataForTime?.metric?.value; + const values = dataForTime?.metric?.values; + if (dataForTime.doc_count === 0) { + obj.results[dataForTime.key] = null; + } else if (value !== undefined) { + obj.results[dataForTime.key] = value; + } else if (values !== undefined) { + // Percentiles agg currently returns NaN rather than null when none of the docs in the + // bucket contain the field used in the aggregation + // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066). + // Store as null, so values can be handled in the same manner downstream as other aggs + // (min, mean, max) which return null. + const medianValues = values[ML_MEDIAN_PERCENTS]; + obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null; + } else { + obj.results[dataForTime.key] = null; + } + } + }); + + return obj; + }) + ); +} + +export interface ModelPlotOutput extends ResultResponse { + results: Record; +} + +export function getModelPlotOutput( + jobId: string, + detectorIndex: number, + criteriaFields: any[], + earliestMs: number, + latestMs: number, + interval: string, + aggType?: { min: any; max: any } +): Observable { + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; + + // if an aggType object has been passed in, use it. + // otherwise default to min and max aggs for the upper and lower bounds + const modelAggs = + aggType === undefined + ? { max: 'max', min: 'min' } + : { + max: aggType.max, + min: aggType.min, + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID and time range. + const mustCriteria: object[] = [ + { + term: { job_id: jobId }, + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ]; + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, criteria => { + mustCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + // Add criteria for the detector index. Results from jobs created before 6.1 will not + // contain a detector_index field, so use a should criteria with a 'not exists' check. + const shouldCriteria = [ + { + term: { detector_index: detectorIndex }, + }, + { + bool: { + must_not: [ + { + exists: { field: 'detector_index' }, + }, + ], + }, + }, + ]; + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:model_plot', + analyze_wildcard: true, + }, + }, + { + bool: { + must: mustCriteria, + should: shouldCriteria, + minimum_should_match: 1, + }, + }, + ], + }, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 0, + }, + aggs: { + actual: { + avg: { + field: 'actual', + }, + }, + modelUpper: { + [modelAggs.max]: { + field: 'model_upper', + }, + }, + modelLower: { + [modelAggs.min]: { + field: 'model_lower', + }, + }, + }, + }, + }, + }, + }) + .pipe( + map(resp => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime: any) => { + const time = dataForTime.key; + const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']); + const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']); + const actual = _.get(dataForTime, ['actual', 'value']); + + obj.results[time] = { + actual, + modelUpper: + modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper, + modelLower: + modelLower === undefined || isFinite(modelLower) === false ? null : modelLower, + }; + }); + + return obj; + }) + ); +} + +export interface RecordsForCriteria extends ResultResponse { + records: any[]; +} + +// Queries Elasticsearch to obtain the record level results matching the given criteria, +// for the specified job(s), time range, and record score threshold. +// criteriaFields parameter must be an array, with each object in the array having 'fieldName' +// 'fieldValue' properties. +// Pass an empty array or ['*'] to search over all job IDs. +export function getRecordsForCriteria( + jobIds: string[] | undefined, + criteriaFields: CriteriaField[], + threshold: any, + earliestMs: number, + latestMs: number, + maxResults: number | undefined +): Observable { + const obj: RecordsForCriteria = { success: true, records: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, criteria => { + boolCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + rest_total_hits_as_int: true, + size: maxResults !== undefined ? maxResults : 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }) + .pipe( + map(resp => { + if (resp.hits.total !== 0) { + _.each(resp.hits.hits, (hit: any) => { + obj.records.push(hit._source); + }); + } + return obj; + }) + ); +} + +export interface ScheduledEventsByBucket extends ResultResponse { + events: Record; +} + +// Obtains a list of scheduled events by job ID and time. +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a events property, which will only +// contains keys for jobs which have scheduled events for the specified time range. +export function getScheduledEventsByBucket( + jobIds: string[] | undefined, + earliestMs: number, + latestMs: number, + interval: string, + maxJobs: number, + maxEvents: number +): Observable { + const obj: ScheduledEventsByBucket = { + success: true, + events: {}, + }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + exists: { field: 'scheduled_events' }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:bucket', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + aggs: { + jobs: { + terms: { + field: 'job_id', + min_doc_count: 1, + size: maxJobs, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 1, + }, + aggs: { + events: { + terms: { + field: 'scheduled_events', + size: maxEvents, + }, + }, + }, + }, + }, + }, + }, + }, + }) + .pipe( + map(resp => { + const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []); + _.each(dataByJobId, (dataForJob: any) => { + const jobId: string = dataForJob.key; + const resultsForTime: Record = {}; + const dataByTime = _.get(dataForJob, ['times', 'buckets'], []); + _.each(dataByTime, (dataForTime: any) => { + const time: string = dataForTime.key; + const events: object[] = _.get(dataForTime, ['events', 'buckets']); + resultsForTime[time] = _.map(events, 'key'); + }); + obj.events[jobId] = resultsForTime; + }); + + return obj; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts new file mode 100644 index 0000000000000..473477a15c2f7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -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. + */ + +export function getScoresByBucket( + jobIds: string[], + earliestMs: number, + latestMs: number, + interval: string | number, + maxResults: number +): Promise; +export function getTopInfluencers(): Promise; +export function getTopInfluencerValues(): Promise; +export function getOverallBucketScores( + jobIds: any, + topN: any, + earliestMs: any, + latestMs: any, + interval?: any +): Promise; +export function getInfluencerValueMaxScoreByTime(): Promise; +export function getRecordInfluencers(): Promise; +export function getRecordsForInfluencer(): Promise; +export function getRecordsForDetector(): Promise; +export function getRecords(): Promise; +export function getEventRateData( + index: string, + query: any, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string | number +): Promise; +export function getEventDistributionData(): Promise; +export function getRecordMaxScoreByTime(): Promise; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service.js b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js similarity index 70% rename from x-pack/legacy/plugins/ml/public/application/services/results_service.js rename to x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js index b0840be4449bf..080ba718964c4 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js @@ -11,17 +11,17 @@ import _ from 'lodash'; // import d3 from 'd3'; -import { ML_MEDIAN_PERCENTS } from '../../../common/util/job_utils'; -import { escapeForElasticsearchQuery } from '../util/string_utils'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { escapeForElasticsearchQuery } from '../../util/string_utils'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; -import { ml } from './ml_api_service'; +import { ml } from '../ml_api_service'; // Obtains the maximum bucket anomaly scores by job ID and time. // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, with a key for job // which has results for the specified time range. -function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { +export function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -141,129 +141,13 @@ function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { }); } -// Obtains a list of scheduled events by job ID and time. -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a events property, which will only -// contains keys for jobs which have scheduled events for the specified time range. -function getScheduledEventsByBucket( - jobIds, - earliestMs, - latestMs, - interval, - maxJobs, - maxEvents) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - events: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - exists: { field: 'scheduled_events' } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:bucket', - analyze_wildcard: false - } - }, { - bool: { - must: boolCriteria - } - }] - } - }, - aggs: { - jobs: { - terms: { - field: 'job_id', - min_doc_count: 1, - size: maxJobs - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 - }, - aggs: { - events: { - terms: { - field: 'scheduled_events', - size: maxEvents - } - } - } - } - } - } - } - } - }) - .then((resp) => { - const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []); - _.each(dataByJobId, (dataForJob) => { - const jobId = dataForJob.key; - const resultsForTime = {}; - const dataByTime = _.get(dataForJob, ['times', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - const time = dataForTime.key; - const events = _.get(dataForTime, ['events', 'buckets']); - resultsForTime[time] = _.map(events, 'key'); - }); - obj.events[jobId] = resultsForTime; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - // Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s). // Pass an empty array or ['*'] to search over all job IDs. // An optional array of influencers may be supplied, with each object in the array having 'fieldName' // and 'fieldValue' properties, to limit data to the supplied list of influencers. // Returned response contains an influencers property, with a key for each of the influencer field names, // whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencers( +export function getTopInfluencers( jobIds, earliestMs, latestMs, @@ -428,7 +312,7 @@ function getTopInfluencers( // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, which is an array of objects // containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) { +export function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) { return new Promise((resolve, reject) => { const obj = { success: true, results: [] }; @@ -528,7 +412,7 @@ function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestM // Obtains the overall bucket scores for the specified job ID(s). // Pass ['*'] to search over all job IDs. // Returned response contains a results property as an object of max score by time. -function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { +export function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; @@ -561,7 +445,7 @@ function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { // values (pass an empty array to search over all field values). // Returned response contains a results property with influencer field values keyed // against max score by time. -function getInfluencerValueMaxScoreByTime( +export function getInfluencerValueMaxScoreByTime( jobIds, influencerFieldName, influencerFieldValues, @@ -720,7 +604,7 @@ function getInfluencerValueMaxScoreByTime( // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a records property, with each record containing // only the fields job_id, detector_index, record_score and influencers. -function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) { +export function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) { return new Promise((resolve, reject) => { const obj = { success: true, records: [] }; @@ -826,7 +710,7 @@ function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResult // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, // so this returns record level results which have at least one of the influencers. // Pass an empty array or ['*'] to search over all job IDs. -function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, latestMs, maxResults, influencersFilterQuery) { +export function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, latestMs, maxResults, influencersFilterQuery) { return new Promise((resolve, reject) => { const obj = { success: true, records: [] }; @@ -949,7 +833,7 @@ function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, lat // Queries Elasticsearch to obtain the record level results for the specified job and detector, // time range, record score threshold, and whether to only return results containing influencers. // An additional, optional influencer field name and value may also be provided. -function getRecordsForDetector( +export function getRecordsForDetector( jobId, detectorIndex, checkForInfluencers, @@ -1076,270 +960,17 @@ function getRecordsForDetector( // and record score threshold. // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a records property, which is an array of the matching results. -function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { +export function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); } -// Queries Elasticsearch to obtain the record level results matching the given criteria, -// for the specified job(s), time range, and record score threshold. -// criteriaFields parameter must be an array, with each object in the array having 'fieldName' -// 'fieldValue' properties. -// Pass an empty array or ['*'] to search over all job IDs. -function getRecordsForCriteria(jobIds, criteriaFields, threshold, earliestMs, latestMs, maxResults) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - record_score: { - gte: threshold, - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - boolCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - rest_total_hits_as_int: true, - size: maxResults !== undefined ? maxResults : 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - sort: [ - { record_score: { order: 'desc' } } - ], - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - _.each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - -// Queries Elasticsearch to obtain metric aggregation results. -// index can be a String, or String[], of index names to search. -// entityFields parameter must be an array, with each object in the array having 'fieldName' -// and 'fieldValue' properties. -// Extra query object can be supplied, or pass null if no additional query -// to that built from the supplied entity fields. -// Returned response contains a results property containing the requested aggregation. -function getMetricData( - index, - entityFields, - query, - metricFunction, // ES aggregation name - metricFieldName, - timeFieldName, - earliestMs, - latestMs, - interval) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: {} }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, entity fields, - // plus any additional supplied query. - const mustCriteria = []; - const shouldCriteria = []; - - mustCriteria.push({ - range: { - [timeFieldName]: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }); - - if (query) { - mustCriteria.push(query); - } - - _.each(entityFields, (entity) => { - if (entity.fieldValue.length !== 0) { - mustCriteria.push({ - term: { - [entity.fieldName]: entity.fieldValue - } - }); - - } else { - // Add special handling for blank entity field values, checking for either - // an empty string or the field not existing. - shouldCriteria.push({ - bool: { - must: [ - { - term: { - [entity.fieldName]: '' - } - } - ] - } - }); - shouldCriteria.push({ - bool: { - must_not: [ - { - exists: { field: entity.fieldName } - } - ] - } - }); - } - - }); - - const body = { - query: { - bool: { - must: mustCriteria - } - }, - size: 0, - _source: { - excludes: [] - }, - aggs: { - byTime: { - date_histogram: { - field: timeFieldName, - interval: interval, - min_doc_count: 0 - } - - } - } - }; - - if (shouldCriteria.length > 0) { - body.query.bool.should = shouldCriteria; - body.query.bool.minimum_should_match = shouldCriteria.length / 2; - } - - if (metricFieldName !== undefined && metricFieldName !== '') { - body.aggs.byTime.aggs = {}; - - const metricAgg = { - [metricFunction]: { - field: metricFieldName - } - }; - - if (metricFunction === 'percentiles') { - metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; - } - body.aggs.byTime.aggs.metric = metricAgg; - } - - ml.esSearch({ - index, - body - }) - .then((resp) => { - const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - if (metricFunction === 'count') { - obj.results[dataForTime.key] = dataForTime.doc_count; - } else { - const value = _.get(dataForTime, ['metric', 'value']); - const values = _.get(dataForTime, ['metric', 'values']); - if (dataForTime.doc_count === 0) { - obj.results[dataForTime.key] = null; - } else if (value !== undefined) { - obj.results[dataForTime.key] = value; - } else if (values !== undefined) { - // Percentiles agg currently returns NaN rather than null when none of the docs in the - // bucket contain the field used in the aggregation - // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066). - // Store as null, so values can be handled in the same manner downstream as other aggs - // (min, mean, max) which return null. - const medianValues = values[ML_MEDIAN_PERCENTS]; - obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null; - } else { - obj.results[dataForTime.key] = null; - } - } - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - // Queries Elasticsearch to obtain event rate data i.e. the count // of documents over time. // index can be a String, or String[], of index names to search. // Extra query object can be supplied, or pass null if no additional query. // Returned response contains a results property, which is an object // of document counts against time (epoch millis). -function getEventRateData( +export function getEventRateData( index, query, timeFieldName, @@ -1420,7 +1051,7 @@ const SAMPLER_TOP_TERMS_SHARD_SIZE = 20000; const ENTITY_AGGREGATION_SIZE = 10; const AGGREGATION_MIN_DOC_COUNT = 1; const CARDINALITY_PRECISION_THRESHOLD = 100; -function getEventDistributionData( +export function getEventDistributionData( index, splitField, filterField = null, @@ -1583,155 +1214,11 @@ function getEventDistributionData( }); } -function getModelPlotOutput( - jobId, - detectorIndex, - criteriaFields, - earliestMs, - latestMs, - interval, - aggType) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // if an aggType object has been passed in, use it. - // otherwise default to min and max aggs for the upper and lower bounds - const modelAggs = (aggType === undefined) ? - { max: 'max', min: 'min' } : - { - max: aggType.max, - min: aggType.min - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID and time range. - const mustCriteria = [ - { - term: { job_id: jobId } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - } - ]; - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - mustCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - // Add criteria for the detector index. Results from jobs created before 6.1 will not - // contain a detector_index field, so use a should criteria with a 'not exists' check. - const shouldCriteria = [ - { - term: { detector_index: detectorIndex } - }, - { - bool: { - must_not: [ - { - exists: { field: 'detector_index' } - } - ] - } - } - ]; - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:model_plot', - analyze_wildcard: true - } - }, { - bool: { - must: mustCriteria, - should: shouldCriteria, - minimum_should_match: 1 - } - }] - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 0 - }, - aggs: { - actual: { - avg: { - field: 'actual' - } - }, - modelUpper: { - [modelAggs.max]: { - field: 'model_upper' - } - }, - modelLower: { - [modelAggs.min]: { - field: 'model_lower' - } - } - } - } - } - } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - let modelUpper = _.get(dataForTime, ['modelUpper', 'value']); - let modelLower = _.get(dataForTime, ['modelLower', 'value']); - const actual = _.get(dataForTime, ['actual', 'value']); - - if (modelUpper === undefined || isFinite(modelUpper) === false) { - modelUpper = null; - } - if (modelLower === undefined || isFinite(modelLower) === false) { - modelLower = null; - } - - obj.results[time] = { - actual, - modelUpper, - modelLower - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - // Queries Elasticsearch to obtain the max record score over time for the specified job, // criteria, time range, and aggregation interval. // criteriaFields parameter must be an array, with each object in the array having 'fieldName' // 'fieldValue' properties. -function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { +export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -1840,22 +1327,3 @@ function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, in }); }); } - -export const mlResultsService = { - getScoresByBucket, - getScheduledEventsByBucket, - getTopInfluencers, - getTopInfluencerValues, - getOverallBucketScores, - getInfluencerValueMaxScoreByTime, - getRecordInfluencers, - getRecordsForInfluencer, - getRecordsForDetector, - getRecords, - getRecordsForCriteria, - getMetricData, - getEventRateData, - getEventDistributionData, - getModelPlotOutput, - getRecordMaxScoreByTime -}; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 5d621a51b710e..eb4dfae3f5ff3 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -1081,10 +1081,6 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo const that = this; function brushed() { - if (that.props.skipRefresh) { - return; - } - const isEmpty = brush.empty(); const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js index 946312d08e9ce..5aa6cfe8835ad 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import './timeseriesexplorer_directive.js'; -import './timeseriesexplorer_route.js'; -import './timeseries_search_service.js'; +import './timeseriesexplorer_directive'; +import './timeseriesexplorer_route'; +import './timeseries_search_service'; import '../components/job_selector'; import '../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts similarity index 69% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 5cbbd530c96f1..65bcc9d355fd6 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -4,44 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ - - import _ from 'lodash'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { ml } from '../services/ml_api_service'; import { isModelPlotEnabled } from '../../../common/util/job_utils'; +// @ts-ignore import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; - -function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, interval) { +import { ModelPlotOutput } from '../services/results_service/result_service_rx'; +import { Job } from '../jobs/new_job/common/job_creator/configs'; + +function getMetricData( + job: Job, + detectorIndex: number, + entityFields: object[], + earliestMs: number, + latestMs: number, + interval: string +): Observable { if (isModelPlotEnabled(job, detectorIndex, entityFields)) { // Extract the partition, by, over fields on which to filter. const criteriaFields = []; const detector = job.analysis_config.detectors[detectorIndex]; if (_.has(detector, 'partition_field_name')) { - const partitionEntity = _.find(entityFields, { 'fieldName': detector.partition_field_name }); + const partitionEntity: any = _.find(entityFields, { + fieldName: detector.partition_field_name, + }); if (partitionEntity !== undefined) { criteriaFields.push( { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, - { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }); + { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue } + ); } } if (_.has(detector, 'over_field_name')) { - const overEntity = _.find(entityFields, { 'fieldName': detector.over_field_name }); + const overEntity: any = _.find(entityFields, { fieldName: detector.over_field_name }); if (overEntity !== undefined) { criteriaFields.push( { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, - { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }); + { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue } + ); } } if (_.has(detector, 'by_field_name')) { - const byEntity = _.find(entityFields, { 'fieldName': detector.by_field_name }); + const byEntity: any = _.find(entityFields, { fieldName: detector.by_field_name }); if (byEntity !== undefined) { criteriaFields.push( { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, - { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }); + { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue } + ); } } @@ -54,15 +69,15 @@ function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, i interval ); } else { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; - const chartConfig = buildConfigFromDetector(job, detectorIndex); + const chartConfig = buildConfigFromDetector(job, detectorIndex); - mlResultsService.getMetricData( + return mlResultsService + .getMetricData( chartConfig.datafeedConfig.indices, entityFields, chartConfig.datafeedConfig.query, @@ -73,20 +88,17 @@ function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, i latestMs, interval ) - .then((resp) => { + .pipe( + map(resp => { _.each(resp.results, (value, time) => { + // @ts-ignore obj.results[time] = { - 'actual': value + actual: value, }; }); - - resolve(obj); + return obj; }) - .catch((resp) => { - reject(resp); - }); - - }); + ); } } @@ -94,9 +106,18 @@ function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, i // in the title area of the time series chart. // Queries Elasticsearch if necessary to obtain the distinct count of entities // for which data is being plotted. -function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) { +function getChartDetails( + job: Job, + detectorIndex: number, + entityFields: any[], + earliestMs: number, + latestMs: number +) { return new Promise((resolve, reject) => { - const obj = { success: true, results: { functionLabel: '', entityData: { entities: [] } } }; + const obj: any = { + success: true, + results: { functionLabel: '', entityData: { entities: [] } }, + }; const chartConfig = buildConfigFromDetector(job, detectorIndex); let functionLabel = chartConfig.metricFunction; @@ -106,7 +127,7 @@ function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) } obj.results.functionLabel = functionLabel; - const blankEntityFields = _.filter(entityFields, (entity) => { + const blankEntityFields = _.filter(entityFields, entity => { return entity.fieldValue.length === 0; }); @@ -124,29 +145,28 @@ function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) query: chartConfig.datafeedConfig.query, timeFieldName: chartConfig.timeField, earliestMs, - latestMs + latestMs, }) - .then((results) => { - _.each(blankEntityFields, (field) => { + .then((results: any) => { + _.each(blankEntityFields, field => { // results will not contain keys for non-aggregatable fields, // so store as 0 to indicate over all field values. obj.results.entityData.entities.push({ fieldName: field.fieldName, - cardinality: _.get(results, field.fieldName, 0) + cardinality: _.get(results, field.fieldName, 0), }); }); resolve(obj); }) - .catch((resp) => { + .catch((resp: any) => { reject(resp); }); } - }); } export const mlTimeSeriesSearchService = { getMetricData, - getChartDetails + getChartDetails, }; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 8492ab11474f5..02e29c1117ffc 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,9 +8,10 @@ * React component for rendering Single Metric Viewer. */ -import { chain, difference, each, find, filter, first, get, has, isEqual, without } from 'lodash'; +import { chain, difference, each, find, first, get, has, isEqual, without } from 'lodash'; import moment from 'moment-timezone'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription, forkJoin } from 'rxjs'; +import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import PropTypes from 'prop-types'; import React, { createRef, Fragment } from 'react'; @@ -78,11 +79,10 @@ import { calculateInitialFocusRange, createTimeSeriesJobData, getAutoZoomDuration, - getFocusData, processForecastResults, processMetricPlotResults, processRecordScoreResults, -} from './timeseriesexplorer_utils'; + getFocusData } from './timeseriesexplorer_utils'; const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); @@ -179,6 +179,11 @@ export class TimeSeriesExplorer extends React.Component { }); } + /** + * Subject for listening brush time range selection. + */ + contextChart$ = new Subject(); + detectorIndexChangeHandler = (e) => { const id = e.target.value; if (id !== undefined) { @@ -252,109 +257,85 @@ export class TimeSeriesExplorer extends React.Component { } contextChartSelectedInitCallDone = false; - contextChartSelected = (selection) => { - const { appStateHandler } = this.props; + /** + * Gets default range from component state. + */ + getDefaultRangeFromState() { const { autoZoomDuration, contextAggregationInterval, contextChartData, contextForecastData, - focusChartData, - jobs, - selectedJob, - zoomFromFocusLoaded, - zoomToFocusLoaded, } = this.state; - - if ((contextChartData === undefined || contextChartData.length === 0) && - (contextForecastData === undefined || contextForecastData.length === 0)) { - return; - } - - const stateUpdate = {}; - - const defaultRange = calculateDefaultFocusRange( + return calculateDefaultFocusRange( autoZoomDuration, contextAggregationInterval, contextChartData, contextForecastData, ); + } - if ((selection.from.getTime() !== defaultRange[0].getTime() || selection.to.getTime() !== defaultRange[1].getTime()) && - (isNaN(Date.parse(selection.from)) === false && isNaN(Date.parse(selection.to)) === false)) { - const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; - appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); - } else { - appStateHandler(APP_STATE_ACTION.UNSET_ZOOM); - } + getFocusAggregationInterval(selection) { + const { + jobs, + selectedJob, + } = this.state; - this.setState({ - zoomFrom: selection.from, - zoomTo: selection.to, - }); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; - if ( - (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || - (zoomFromFocusLoaded.getTime() !== selection.from.getTime()) || - (zoomToFocusLoaded.getTime() !== selection.to.getTime()) - ) { - this.contextChartSelectedInitCallDone = true; + return calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + jobs, + selectedJob, + ); + } - // Calculate the aggregation interval for the focus chart. - const bounds = { min: moment(selection.from), max: moment(selection.to) }; - const focusAggregationInterval = calculateAggregationInterval( - bounds, - CHARTS_POINT_TARGET, - jobs, - selectedJob, - ); - stateUpdate.focusAggregationInterval = focusAggregationInterval; + /** + * Gets focus data for the current component state/ + */ + getFocusData(selection) { + const { + detectorId, + entities, + modelPlotEnabled, + selectedJob, + } = this.state; - // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. - // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected - // to some extent with all detector functions if not searching complete buckets. - const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + const { appStateHandler } = this.props; - const { - detectorId, - entities, - modelPlotEnabled, - } = this.state; - - this.setState({ - loading: true, - fullRefresh: false, - zoomFrom: selection.from, - zoomTo: selection.to, - }); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; - getFocusData( - this._criteriaFields, - +detectorId, - focusAggregationInterval, - appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID), - modelPlotEnabled, - filter(entities, entity => entity.fieldValue.length > 0), - searchBounds, - selectedJob, - TIME_FIELD_NAME, - ).then((refreshFocusData) => { - // All the data is ready now for a state update. - this.setState({ - ...stateUpdate, - ...refreshFocusData, - loading: false, - showModelBoundsCheckbox: (modelPlotEnabled === true) && (refreshFocusData.focusChartData.length > 0), - zoomFromFocusLoaded: selection.from, - zoomToFocusLoaded: selection.to, - }); - }); + const focusAggregationInterval = this.getFocusAggregationInterval(selection); - // Load the data for the anomalies table. - this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()); - } + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = getBoundsRoundedToInterval( + bounds, + focusAggregationInterval, + false + ); + + return getFocusData( + this._criteriaFields, + +detectorId, + focusAggregationInterval, + appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID), + modelPlotEnabled, + entities.filter(entity => entity.fieldValue.length > 0), + searchBounds, + selectedJob, + TIME_FIELD_NAME + ); + } + + contextChartSelected = (selection) => { + this.contextChart$.next(selection); } entityFieldValueChanged = (entity, fieldValue) => { @@ -380,7 +361,7 @@ export class TimeSeriesExplorer extends React.Component { const { dateFormatTz } = this.props; const { selectedJob } = this.state; - ml.results.getAnomaliesTableData( + return ml.results.getAnomaliesTableData( [selectedJob.job_id], this._criteriaFields, [], @@ -390,43 +371,43 @@ export class TimeSeriesExplorer extends React.Component { latestMs, dateFormatTz, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE - ).then((resp) => { - const anomalies = resp.anomalies; - const detectorsByJob = mlJobService.detectorsByJob; - anomalies.forEach((anomaly) => { - // Add a detector property to each anomaly. - // Default to functionDescription if no description available. - // TODO - when job_service is moved server_side, move this to server endpoint. - const jobId = anomaly.jobId; - const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); - anomaly.detector = get(detector, - ['detector_description'], - anomaly.source.function_description); - - // For detectors with rules, add a property with the rule count. - const customRules = detector.custom_rules; - if (customRules !== undefined) { - anomaly.rulesLength = customRules.length; - } + ).pipe( + map(resp => { + const anomalies = resp.anomalies; + const detectorsByJob = mlJobService.detectorsByJob; + anomalies.forEach((anomaly) => { + // Add a detector property to each anomaly. + // Default to functionDescription if no description available. + // TODO - when job_service is moved server_side, move this to server endpoint. + const jobId = anomaly.jobId; + const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); + anomaly.detector = get(detector, + ['detector_description'], + anomaly.source.function_description); + + // For detectors with rules, add a property with the rule count. + const customRules = detector.custom_rules; + if (customRules !== undefined) { + anomaly.rulesLength = customRules.length; + } - // Add properties used for building the links menu. - // TODO - when job_service is moved server_side, move this to server endpoint. - if (has(mlJobService.customUrlsByJob, jobId)) { - anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; - } - }); + // Add properties used for building the links menu. + // TODO - when job_service is moved server_side, move this to server endpoint. + if (has(mlJobService.customUrlsByJob, jobId)) { + anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; + } + }); - this.setState({ - tableData: { - anomalies, - interval: resp.interval, - examplesByJobId: resp.examplesByJobId, - showViewSeriesLink: false - } - }); - }).catch((resp) => { - console.log('Time series explorer - error loading data for anomalies table:', resp); - }); + return { + tableData: { + anomalies, + interval: resp.interval, + examplesByJobId: resp.examplesByJobId, + showViewSeriesLink: false + } + }; + }) + ); } loadEntityValues = (callback = () => {}) => { @@ -445,6 +426,7 @@ export class TimeSeriesExplorer extends React.Component { bounds.min.valueOf(), bounds.max.valueOf(), ANOMALIES_TABLE_DEFAULT_QUERY_SIZE) + .toPromise() .then((resp) => { if (resp.records && resp.records.length > 0) { const firstRec = resp.records[0]; @@ -604,7 +586,7 @@ export class TimeSeriesExplorer extends React.Component { } }; - const nonBlankEntities = filter(currentEntities, (entity) => { return entity.fieldValue.length > 0; }); + const nonBlankEntities = currentEntities.filter((entity) => { return entity.fieldValue.length > 0; }); if (modelPlotEnabled === false && isSourceDataChartableForDetector(selectedJob, detectorIndex) === false && @@ -646,7 +628,7 @@ export class TimeSeriesExplorer extends React.Component { searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.expression - ).then((resp) => { + ).toPromise().then((resp) => { const fullRangeChartData = processMetricPlotResults(resp.results, modelPlotEnabled); stateUpdate.contextChartData = fullRangeChartData; finish(counter); @@ -702,7 +684,8 @@ export class TimeSeriesExplorer extends React.Component { searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.expression, - aggType) + aggType + ).toPromise() .then((resp) => { stateUpdate.contextForecastData = processForecastResults(resp.results); finish(counter); @@ -762,7 +745,7 @@ export class TimeSeriesExplorer extends React.Component { */ updateCriteriaFields(detectorIndex, entities) { // Only filter on the entity if the field has a value. - const nonBlankEntities = filter(entities, (entity) => { return entity.fieldValue.length > 0; }); + const nonBlankEntities = entities.filter(entity => entity.fieldValue.length > 0); this._criteriaFields = [ { fieldName: 'detector_index', @@ -868,7 +851,8 @@ export class TimeSeriesExplorer extends React.Component { const tableControlsListener = () => { const { zoomFrom, zoomTo } = this.state; if (zoomFrom !== undefined && zoomTo !== undefined) { - this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()); + this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()).subscribe(res => + this.setState(res)); } }; @@ -978,6 +962,97 @@ export class TimeSeriesExplorer extends React.Component { this.resizeHandler(); }); this.resizeHandler(); + + // Listen for context chart updates. + this.subscriptions.add(this.contextChart$ + .pipe( + tap(selection => { + this.setState({ + zoomFrom: selection.from, + zoomTo: selection.to, + }); + }), + debounceTime(500), + tap((selection) => { + const { + contextChartData, + contextForecastData, + focusChartData, + zoomFromFocusLoaded, + zoomToFocusLoaded, + } = this.state; + + if ((contextChartData === undefined || contextChartData.length === 0) && + (contextForecastData === undefined || contextForecastData.length === 0)) { + return; + } + + const defaultRange = this.getDefaultRangeFromState(); + + if ((selection.from.getTime() !== defaultRange[0].getTime() || selection.to.getTime() !== defaultRange[1].getTime()) && + (isNaN(Date.parse(selection.from)) === false && isNaN(Date.parse(selection.to)) === false)) { + const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; + appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); + } else { + appStateHandler(APP_STATE_ACTION.UNSET_ZOOM); + } + + if ( + (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || + (zoomFromFocusLoaded.getTime() !== selection.from.getTime()) || + (zoomToFocusLoaded.getTime() !== selection.to.getTime()) + ) { + this.contextChartSelectedInitCallDone = true; + + this.setState({ + loading: true, + fullRefresh: false, + }); + } + }), + switchMap(selection => { + const { + jobs, + selectedJob + } = this.state; + + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; + const focusAggregationInterval = calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + jobs, + selectedJob, + ); + + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + return forkJoin([ + this.getFocusData(selection), + // Load the data for the anomalies table. + this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()) + ]); + }), + withLatestFrom(this.contextChart$) + ) + .subscribe(([[refreshFocusData, tableData], selection]) => { + const { + modelPlotEnabled, + } = this.state; + + // All the data is ready now for a state update. + this.setState({ + focusAggregationInterval: this.getFocusAggregationInterval({ from: selection.from, to: selection.to }), + loading: false, + showModelBoundsCheckbox: modelPlotEnabled && (refreshFocusData.focusChartData.length > 0), + zoomFromFocusLoaded: selection.from, + zoomToFocusLoaded: selection.to, + ...refreshFocusData, + ...tableData + }); + })); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts index 52590bb6824c1..29a5facf64c0f 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts @@ -8,7 +8,6 @@ * Contains values for ML time series explorer. */ - export const APP_STATE_ACTION = { CLEAR: 'CLEAR', GET_DETECTOR_INDEX: 'GET_DETECTOR_INDEX', diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts new file mode 100644 index 0000000000000..03fe718de9bed --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -0,0 +1,164 @@ +/* + * 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 { forkJoin, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import chrome from 'ui/chrome'; +import { ml } from '../../services/ml_api_service'; +import { + ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, +} from '../../../../common/constants/search'; +import { mlTimeSeriesSearchService } from '../timeseries_search_service'; +import { mlResultsService, CriteriaField } from '../../services/results_service'; +import { Job } from '../../jobs/new_job/common/job_creator/configs'; +import { MAX_SCHEDULED_EVENTS, TIME_FIELD_NAME } from '../timeseriesexplorer_constants'; +import { + processDataForFocusAnomalies, + processForecastResults, + processMetricPlotResults, + processScheduledEventsForChart, +} from './timeseriesexplorer_utils'; +import { mlForecastService } from '../../services/forecast_service'; +import { mlFunctionToESAggregation } from '../../../../common/util/job_utils'; +import { Annotation } from '../../../../common/types/annotations'; + +const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); + +export interface Interval { + asMilliseconds: () => number; + expression: string; +} + +export interface FocusData { + focusChartData: any; + anomalyRecords: any; + scheduledEvents: any; + showForecastCheckbox?: any; + focusAnnotationData?: any; + focusForecastData?: any; +} + +export function getFocusData( + criteriaFields: CriteriaField[], + detectorIndex: number, + focusAggregationInterval: Interval, + forecastId: string, + modelPlotEnabled: boolean, + nonBlankEntities: any[], + searchBounds: any, + selectedJob: Job +): Observable { + return forkJoin([ + // Query 1 - load metric data across selected time range. + mlTimeSeriesSearchService.getMetricData( + selectedJob, + detectorIndex, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression + ), + // Query 2 - load all the records across selected time range for the chart anomaly markers. + mlResultsService.getRecordsForCriteria( + [selectedJob.job_id], + criteriaFields, + 0, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE + ), + // Query 3 - load any scheduled events for the selected job. + mlResultsService.getScheduledEventsByBucket( + [selectedJob.job_id], + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + 1, + MAX_SCHEDULED_EVENTS + ), + // Query 4 - load any annotations for the selected job. + mlAnnotationsEnabled + ? ml.annotations + .getAnnotations({ + jobIds: [selectedJob.job_id], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + }) + .pipe( + catchError(() => { + // silent fail + return of({ annotations: {} as Record }); + }) + ) + : of(null), + // Plus query for forecast data if there is a forecastId stored in the appState. + forecastId !== undefined + ? (() => { + let aggType; + const detector = selectedJob.analysis_config.detectors[detectorIndex]; + const esAgg = mlFunctionToESAggregation(detector.function); + if (!modelPlotEnabled && (esAgg === 'sum' || esAgg === 'count')) { + aggType = { avg: 'sum', max: 'sum', min: 'sum' }; + } + return mlForecastService.getForecastData( + selectedJob, + detectorIndex, + forecastId, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + aggType + ); + })() + : of(null), + ]).pipe( + map(([metricData, recordsForCriteria, scheduledEventsByBucket, annotations, forecastData]) => { + // Sort in descending time order before storing in scope. + const anomalyRecords = recordsForCriteria.records + .sort((a, b) => a[TIME_FIELD_NAME] - b[TIME_FIELD_NAME]) + .reverse(); + + const scheduledEvents = scheduledEventsByBucket.events[selectedJob.job_id]; + + let focusChartData = processMetricPlotResults(metricData.results, modelPlotEnabled); + // Tell the results container directives to render the focus chart. + focusChartData = processDataForFocusAnomalies( + focusChartData, + anomalyRecords, + focusAggregationInterval, + modelPlotEnabled + ); + focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents); + + const refreshFocusData: FocusData = { + scheduledEvents, + anomalyRecords, + focusChartData, + }; + + if (annotations) { + refreshFocusData.focusAnnotationData = (annotations.annotations[selectedJob.job_id] ?? []) + .sort((a, b) => { + return a.timestamp - b.timestamp; + }) + .map((d, i) => { + d.key = String.fromCharCode(65 + i); + return d; + }); + } + + if (forecastData) { + refreshFocusData.focusForecastData = processForecastResults(forecastData.results); + refreshFocusData.showForecastCheckbox = refreshFocusData.focusForecastData.length > 0; + } + + return refreshFocusData; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts new file mode 100644 index 0000000000000..578dbdf1277a0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/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 { getFocusData } from './get_focus_data'; +export * from './timeseriesexplorer_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts new file mode 100644 index 0000000000000..1528ac887ad76 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function createTimeSeriesJobData(jobs: any): any; + +export function processMetricPlotResults(metricPlotData: any, modelPlotEnabled: any): any; + +export function processForecastResults(forecastData: any): any; + +export function processRecordScoreResults(scoreData: any): any; + +export function processDataForFocusAnomalies( + chartData: any, + anomalyRecords: any, + aggregationInterval: any, + modelPlotEnabled: any +): any; + +export function processScheduledEventsForChart(chartData: any, scheduledEvents: any): any; + +export function findNearestChartPointToTime(chartData: any, time: any): any; + +export function findChartPointForAnomalyTime( + chartData: any, + anomalyTime: any, + aggregationInterval: any +): any; + +export function calculateAggregationInterval( + bounds: any, + bucketsTarget: any, + jobs: any, + selectedJob: any +): any; + +export function calculateDefaultFocusRange( + autoZoomDuration: any, + contextAggregationInterval: any, + contextChartData: any, + contextForecastData: any +): any; + +export function calculateInitialFocusRange( + zoomState: any, + contextAggregationInterval: any, + timefilter: any +): any; + +export function getAutoZoomDuration(jobs: any, selectedJob: any): any; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js similarity index 70% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js index 61f5a76a5877e..b9c9ed87ddbc7 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -15,31 +15,17 @@ import _ from 'lodash'; import moment from 'moment-timezone'; -import { - ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../../common/constants/search'; import { isTimeSeriesViewJob, - mlFunctionToESAggregation, -} from '../../../common/util/job_utils'; -import { parseInterval } from '../../../common/util/parse_interval'; - -import { ml } from '../services/ml_api_service'; -import { mlForecastService } from '../services/forecast_service'; -import { mlResultsService } from '../services/results_service'; -import { TimeBuckets, getBoundsRoundedToInterval } from '../util/time_buckets'; +} from '../../../../common/util/job_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; -import { mlTimeSeriesSearchService } from './timeseries_search_service'; +import { TimeBuckets, getBoundsRoundedToInterval } from '../../util/time_buckets'; import { CHARTS_POINT_TARGET, - MAX_SCHEDULED_EVENTS, TIME_FIELD_NAME, -} from './timeseriesexplorer_constants'; - -import chrome from 'ui/chrome'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); +} from '../timeseriesexplorer_constants'; // create new job objects based on standard job config objects // new job objects just contain job id, bucket span in seconds and a selected flag. @@ -301,154 +287,6 @@ export function findChartPointForAnomalyTime(chartData, anomalyTime, aggregation return chartPoint; } -export const getFocusData = function ( - criteriaFields, - detectorIndex, - focusAggregationInterval, - forecastId, - modelPlotEnabled, - nonBlankEntities, - searchBounds, - selectedJob, -) { - return new Promise((resolve, reject) => { - // Counter to keep track of the queries to populate the chart. - let awaitingCount = 4; - - // This object is used to store the results of individual remote requests - // before we transform it into the final data and apply it to $scope. Otherwise - // we might trigger multiple $digest cycles and depending on how deep $watches - // listen for changes we could miss updates. - const refreshFocusData = {}; - - // finish() function, called after each data set has been loaded and processed. - // The last one to call it will trigger the page render. - function finish() { - awaitingCount--; - if (awaitingCount === 0) { - // Tell the results container directives to render the focus chart. - refreshFocusData.focusChartData = processDataForFocusAnomalies( - refreshFocusData.focusChartData, - refreshFocusData.anomalyRecords, - focusAggregationInterval, - modelPlotEnabled, - ); - - refreshFocusData.focusChartData = processScheduledEventsForChart( - refreshFocusData.focusChartData, - refreshFocusData.scheduledEvents); - - resolve(refreshFocusData); - } - } - - // Query 1 - load metric data across selected time range. - mlTimeSeriesSearchService.getMetricData( - selectedJob, - detectorIndex, - nonBlankEntities, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression - ).then((resp) => { - refreshFocusData.focusChartData = processMetricPlotResults(resp.results, modelPlotEnabled); - finish(); - }).catch((resp) => { - console.log('Time series explorer - error getting metric data from elasticsearch:', resp); - reject(); - }); - - // Query 2 - load all the records across selected time range for the chart anomaly markers. - mlResultsService.getRecordsForCriteria( - [selectedJob.job_id], - criteriaFields, - 0, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE - ).then((resp) => { - // Sort in descending time order before storing in scope. - refreshFocusData.anomalyRecords = _.chain(resp.records) - .sortBy(record => record[TIME_FIELD_NAME]) - .reverse() - .value(); - finish(); - }); - - // Query 3 - load any scheduled events for the selected job. - mlResultsService.getScheduledEventsByBucket( - [selectedJob.job_id], - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression, - 1, - MAX_SCHEDULED_EVENTS - ).then((resp) => { - refreshFocusData.scheduledEvents = resp.events[selectedJob.job_id]; - finish(); - }).catch((resp) => { - console.log('Time series explorer - error getting scheduled events from elasticsearch:', resp); - reject(); - }); - - // Query 4 - load any annotations for the selected job. - if (mlAnnotationsEnabled) { - ml.annotations.getAnnotations({ - jobIds: [selectedJob.job_id], - earliestMs: searchBounds.min.valueOf(), - latestMs: searchBounds.max.valueOf(), - maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { - refreshFocusData.focusAnnotationData = resp.annotations[selectedJob.job_id] - .sort((a, b) => { - return a.timestamp - b.timestamp; - }) - .map((d, i) => { - d.key = String.fromCharCode(65 + i); - return d; - }); - - finish(); - }).catch(() => { - // silent fail - refreshFocusData.focusAnnotationData = []; - finish(); - }); - } else { - finish(); - } - - // Plus query for forecast data if there is a forecastId stored in the appState. - if (forecastId !== undefined) { - awaitingCount++; - let aggType = undefined; - const detector = selectedJob.analysis_config.detectors[detectorIndex]; - const esAgg = mlFunctionToESAggregation(detector.function); - if (modelPlotEnabled === false && (esAgg === 'sum' || esAgg === 'count')) { - aggType = { avg: 'sum', max: 'sum', min: 'sum' }; - } - - mlForecastService.getForecastData( - selectedJob, - detectorIndex, - forecastId, - nonBlankEntities, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression, - aggType) - .then((resp) => { - refreshFocusData.focusForecastData = processForecastResults(resp.results); - refreshFocusData.showForecastCheckbox = (refreshFocusData.focusForecastData.length > 0); - finish(); - }).catch((resp) => { - console.log(`Time series explorer - error loading data for forecast ID ${forecastId}`, resp); - reject(); - }); - } - }); -}; - export function calculateAggregationInterval( bounds, bucketsTarget, From 8282450f8fa2c4f3164e39ecd5b18fa422e2ad6d Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 25 Nov 2019 13:29:49 +0100 Subject: [PATCH 03/63] [SIEM] Fix typo in Palo Alto Networks (#51570) --- .../components/page/overview/overview_network_stats/index.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx index cee2c18710e74..8f592c7bbba60 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx @@ -73,7 +73,7 @@ const overviewNetworkStats = (data: OverviewNetworkData) => [ title: ( ), id: 'filebeatPanw', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3dd141a164e3e..f35a650cdf1b6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10595,7 +10595,7 @@ "xpack.siem.overview.feedbackTitle": "フィードバック", "xpack.siem.overview.filebeatCiscoTitle": "Filebeat Cisco", "xpack.siem.overview.filebeatNetflowTitle": "Filebeat Netflow", - "xpack.siem.overview.filebeatPanwTitle": "Filebeat Palo Alto Network", + "xpack.siem.overview.filebeatPanwTitle": "Filebeat Palo Alto Networks", "xpack.siem.overview.fileBeatSuricataTitle": "Filebeat Suricata", "xpack.siem.overview.filebeatSystemModuleTitle": "Filebeat システムモジュール", "xpack.siem.overview.fileBeatZeekTitle": "Filebeat Zeek", From aef6dd4707b19babd28bb93164bdd61d8029a89a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Nov 2019 09:11:08 -0500 Subject: [PATCH 04/63] Upgrade handlebars to 4.5.3 (#51486) Co-authored-by: Elastic Machine --- package.json | 2 +- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a1873134e3cb8..a4f7b869aef6f 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "globby": "^8.0.1", "good-squeeze": "2.1.0", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", diff --git a/x-pack/package.json b/x-pack/package.json index 84ce92bf8e9e6..f84db22fe5c40 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -244,7 +244,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", diff --git a/yarn.lock b/yarn.lock index 64d33426d7aa4..3296fc013c48d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14310,10 +14310,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.3.5.tgz#d6c2d0a0f08b4479e3949f8321c0f3893bb691be" - integrity sha512-I16T/l8X9DV3sEkY9sK9lsPRgDsj82ayBY/4pAZyP2BcX5WeRM3O06bw9kIs2GLrHvFB/DNzWWJyFvof8wQGqw== +handlebars@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" From a1b01f4a7582dd35f236393672130871a0e03f56 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 25 Nov 2019 15:14:55 +0100 Subject: [PATCH 05/63] - Update _template completion to work even when the index does not exist (#51592) - Update _template body completions --- .../components/template_autocomplete_component.js | 2 +- .../spec/overrides/indices.put_template.json | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js index e6cae3f1710bc..0c00b2f93ee6f 100644 --- a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js +++ b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js @@ -21,7 +21,7 @@ import { ListComponent } from './list_component'; export class TemplateAutocompleteComponent extends ListComponent { constructor(name, parent) { - super(name, mappings.getTemplates, parent); + super(name, mappings.getTemplates, parent, true, true); } getContextKey() { return 'template'; diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json index cc7218be2e48d..c19836e2f9eb0 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json @@ -1,10 +1,16 @@ { "indices.put_template": { "data_autocomplete_rules": { - "template": "index*", - "warmers": { "__scope_link": "_warmer" }, + "index_patterns": [], "mappings": { "__scope_link": "put_mapping" }, - "settings": { "__scope_link": "put_settings" } + "settings": { "__scope_link": "put_settings" }, + "version": 0, + "order": 0, + "aliases": { + "__template": { + "NAME": {} + } + } }, "patterns": [ "_template/{template}" From ce2dd0dc669af17ae84016ef374884d5be28ef1f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 25 Nov 2019 15:18:34 +0100 Subject: [PATCH 06/63] Add cumulative cardinality per https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-cardinality-aggregation.html (#51591) --- .../console/server/api_server/es_6_0/aggregations.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js index e54bc2476698a..c2596dc4258da 100644 --- a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js +++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js @@ -352,6 +352,13 @@ const rules = { }, missing: '', }, + cumulative_cardinality: { + __template: { + buckets_path: '', + }, + buckets_path: '', + format: '', + }, scripted_metric: { __template: { init_script: '', From 6f01aa962e3f7fe6c57d3d0ac7337c595e4e3fc9 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 25 Nov 2019 08:49:36 -0600 Subject: [PATCH 07/63] EUI i18n token updates (#51307) * updated i18n tokens for eui * i18n snapshot * translation file removals * fix typo --- .../__snapshots__/i18n_service.test.tsx.snap | 56 +++- src/core/public/i18n/i18n_service.tsx | 271 +++++++++++++++++- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 4 files changed, 314 insertions(+), 21 deletions(-) diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index d159c588718fe..d0374511515d1 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -10,6 +10,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiBasicTable.selectThisRow": "Select this row", "euiBasicTable.tableDescription": [Function], "euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.", + "euiBreadcrumbs.collapsedBadge.ariaLabel": "Show all breadcrumbs", "euiCardSelect.select": "Select", "euiCardSelect.selected": "Selected", "euiCardSelect.unavailable": "Unavailable", @@ -21,6 +22,20 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiCollapsedItemActions.allActions": "All actions", "euiColorPicker.screenReaderAnnouncement": "A popup with a range of selectable colors opened. Tab forward to cycle through colors choices or press escape to close this popup.", "euiColorPicker.swatchAriaLabel": [Function], + "euiColorStopThumb.removeLabel": "Remove this stop", + "euiColorStopThumb.screenReaderAnnouncement": "A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.", + "euiColorStops.screenReaderAnnouncement": [Function], + "euiColumnSelector.hideAll": "Hide all", + "euiColumnSelector.selectAll": "Show all", + "euiColumnSorting.clearAll": "Clear sorting", + "euiColumnSorting.emptySorting": "Currently no fields are sorted", + "euiColumnSorting.pickFields": "Pick fields to sort by", + "euiColumnSorting.sortFieldAriaLabel": "Sort by:", + "euiColumnSortingDraggable.activeSortLabel": "is sorting this data grid", + "euiColumnSortingDraggable.defaultSortAsc": "A-Z", + "euiColumnSortingDraggable.defaultSortDesc": "Z-A", + "euiColumnSortingDraggable.removeSortLabel": "Remove from data grid sort:", + "euiColumnSortingDraggable.toggleLegend": "Select sorting method for field:", "euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options", "euiComboBoxOptionsList.alreadyAdded": [Function], "euiComboBoxOptionsList.createCustomOption": [Function], @@ -28,6 +43,19 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available", "euiComboBoxOptionsList.noMatchingOptions": [Function], "euiComboBoxPill.removeSelection": [Function], + "euiCommonlyUsedTimeRanges.legend": "Commonly used", + "euiDataGrid.screenReaderNotice": "Cell contains interactive content.", + "euiDataGridCell.expandButtonTitle": "Click or hit enter to interact with cell content", + "euiDataGridSchema.booleanSortTextAsc": "True-False", + "euiDataGridSchema.booleanSortTextDesc": "False-True", + "euiDataGridSchema.currencySortTextAsc": "Low-High", + "euiDataGridSchema.currencySortTextDesc": "High-Low", + "euiDataGridSchema.dateSortTextAsc": "New-Old", + "euiDataGridSchema.dateSortTextDesc": "Old-New", + "euiDataGridSchema.jsonSortTextAsc": "Small-Large", + "euiDataGridSchema.jsonSortTextDesc": "Large-Small", + "euiDataGridSchema.numberSortTextAsc": "Low-High", + "euiDataGridSchema.numberSortTextDesc": "High-Low", "euiFilterButton.filterBadge": [Function], "euiForm.addressFormErrors": "Please address the errors in your form.", "euiFormControlLayoutClearButton.label": "Clear input", @@ -35,25 +63,45 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiHeaderLinks.appNavigation": "App navigation", "euiHeaderLinks.openNavigationMenu": "Open navigation menu", "euiHue.label": "Select the HSV color mode \\"hue\\" value", + "euiImage.closeImage": [Function], + "euiImage.openImage": [Function], + "euiLink.external.ariaLabel": "External link", "euiModal.closeModal": "Closes this modal window", "euiPagination.jumpToLastPage": [Function], "euiPagination.nextPage": "Next page", "euiPagination.pageOfTotal": [Function], "euiPagination.previousPage": "Previous page", - "euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.", + "euiPopover.screenReaderAnnouncement": "You are in a dialog. To close this dialog, hit escape.", + "euiQuickSelect.applyButton": "Apply", + "euiQuickSelect.fullDescription": [Function], + "euiQuickSelect.legendText": "Quick select a time range", + "euiQuickSelect.nextLabel": "Next time window", + "euiQuickSelect.previousLabel": "Previous time window", + "euiQuickSelect.quickSelectTitle": "Quick select", + "euiQuickSelect.tenseLabel": "Time tense", + "euiQuickSelect.unitLabel": "Time unit", + "euiQuickSelect.valueLabel": "Time value", + "euiRefreshInterval.fullDescription": [Function], + "euiRefreshInterval.legend": "Refresh every", + "euiRefreshInterval.start": "Start", + "euiRefreshInterval.stop": "Stop", + "euiRelativeTab.fullDescription": [Function], + "euiRelativeTab.relativeDate": [Function], + "euiRelativeTab.roundingLabel": [Function], + "euiRelativeTab.unitInputLabel": "Relative time span", "euiSaturation.roleDescription": "HSV color mode saturation and value selection", "euiSaturation.screenReaderAnnouncement": "Use the arrow keys to navigate the square color gradient. The coordinates resulting from each key press will be used to calculate HSV color mode \\"saturation\\" and \\"value\\" numbers, in the range of 0 to 1. Left and right decrease and increase (respectively) the \\"saturation\\" value. Up and down decrease and increase (respectively) the \\"value\\" value.", "euiSelectable.loadingOptions": "Loading options", "euiSelectable.noAvailableOptions": "There aren't any options available", "euiSelectable.noMatchingOptions": [Function], "euiStat.loadingText": "Statistic is loading", - "euiStep.completeStep": "Step", - "euiStep.incompleteStep": "Incomplete Step", + "euiStep.ariaLabel": [Function], "euiStepHorizontal.buttonTitle": [Function], "euiStepHorizontal.step": "Step", "euiStepNumber.hasErrors": "has errors", "euiStepNumber.hasWarnings": "has warnings", "euiStepNumber.isComplete": "complete", + "euiStyleSelector.buttonText": "Density", "euiSuperDatePicker.showDatesButtonLabel": "Show dates", "euiSuperSelect.screenReaderAnnouncement": [Function], "euiSuperSelectControl.selectAnOption": [Function], @@ -68,6 +116,8 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiToast.dismissToast": "Dismiss toast", "euiToast.newNotification": "A new notification appears", "euiToast.notification": "Notification", + "euiTreeView.ariaLabel": [Function], + "euiTreeView.listNavigationInstructions": "You can quickly navigate this list using arrow keys.", }, } } diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 17cdf56cd43c0..721c5d49634f4 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -65,6 +65,13 @@ export class I18nService { 'Screen reader announcement that functionality is available in the page document', } ), + 'euiBreadcrumbs.collapsedBadge.ariaLabel': i18n.translate( + 'core.euiBreadcrumbs.collapsedBadge.ariaLabel', + { + defaultMessage: 'Show all breadcrumbs', + description: 'Displayed when one or more breadcrumbs are hidden.', + } + ), 'euiCardSelect.select': i18n.translate('core.euiCardSelect.select', { defaultMessage: 'Select', description: 'Displayed button text when a card option can be selected.', @@ -117,6 +124,80 @@ export class I18nService { description: 'Screen reader text to describe the action and hex value of the selectable option', }), + 'euiColorStopThumb.removeLabel': i18n.translate('core.euiColorStopThumb.removeLabel', { + defaultMessage: 'Remove this stop', + description: 'Label accompanying a button whose action will remove the color stop', + }), + 'euiColorStopThumb.screenReaderAnnouncement': i18n.translate( + 'core.euiColorStopThumb.screenReaderAnnouncement', + { + defaultMessage: + 'A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.', + description: + 'Message when the color picker popover has opened for an individual color stop thumb.', + } + ), + 'euiColorStops.screenReaderAnnouncement': ({ label, readOnly, disabled }: EuiValues) => + i18n.translate('core.euiColorStops.screenReaderAnnouncement', { + defaultMessage: + '{label}: {readOnly} {disabled} Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop.', + values: { label, readOnly, disabled }, + description: + 'Screen reader text to describe the composite behavior of the color stops component.', + }), + 'euiColumnSelector.hideAll': i18n.translate('core.euiColumnSelector.hideAll', { + defaultMessage: 'Hide all', + }), + 'euiColumnSelector.selectAll': i18n.translate('core.euiColumnSelector.selectAll', { + defaultMessage: 'Show all', + }), + 'euiColumnSorting.clearAll': i18n.translate('core.euiColumnSorting.clearAll', { + defaultMessage: 'Clear sorting', + }), + 'euiColumnSorting.emptySorting': i18n.translate('core.euiColumnSorting.emptySorting', { + defaultMessage: 'Currently no fields are sorted', + }), + 'euiColumnSorting.pickFields': i18n.translate('core.euiColumnSorting.pickFields', { + defaultMessage: 'Pick fields to sort by', + }), + 'euiColumnSorting.sortFieldAriaLabel': i18n.translate( + 'core.euiColumnSorting.sortFieldAriaLabel', + { + defaultMessage: 'Sort by:', + } + ), + 'euiColumnSortingDraggable.activeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.activeSortLabel', + { + defaultMessage: 'is sorting this data grid', + } + ), + 'euiColumnSortingDraggable.defaultSortAsc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortAsc', + { + defaultMessage: 'A-Z', + description: 'Ascending sort label', + } + ), + 'euiColumnSortingDraggable.defaultSortDesc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortDesc', + { + defaultMessage: 'Z-A', + description: 'Descending sort label', + } + ), + 'euiColumnSortingDraggable.removeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.removeSortLabel', + { + defaultMessage: 'Remove from data grid sort:', + } + ), + 'euiColumnSortingDraggable.toggleLegend': i18n.translate( + 'core.euiColumnSortingDraggable.toggleLegend', + { + defaultMessage: 'Select sorting method for field:', + } + ), 'euiComboBoxOptionsList.allOptionsSelected': i18n.translate( 'core.euiComboBoxOptionsList.allOptionsSelected', { @@ -163,6 +244,88 @@ export class I18nService { values: { children }, description: 'ARIA label, `children` is the human-friendly value of an option', }), + 'euiCommonlyUsedTimeRanges.legend': i18n.translate('core.euiCommonlyUsedTimeRanges.legend', { + defaultMessage: 'Commonly used', + }), + 'euiDataGrid.screenReaderNotice': i18n.translate('core.euiDataGrid.screenReaderNotice', { + defaultMessage: 'Cell contains interactive content.', + }), + 'euiDataGridCell.expandButtonTitle': i18n.translate( + 'core.euiDataGridCell.expandButtonTitle', + { + defaultMessage: 'Click or hit enter to interact with cell content', + } + ), + 'euiDataGridSchema.booleanSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextAsc', + { + defaultMessage: 'True-False', + description: 'Ascending boolean label', + } + ), + 'euiDataGridSchema.booleanSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextDesc', + { + defaultMessage: 'False-True', + description: 'Descending boolean label', + } + ), + 'euiDataGridSchema.currencySortTextAsc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending currency label', + } + ), + 'euiDataGridSchema.currencySortTextDesc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending currency label', + } + ), + 'euiDataGridSchema.dateSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextAsc', + { + defaultMessage: 'New-Old', + description: 'Ascending date label', + } + ), + 'euiDataGridSchema.dateSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextDesc', + { + defaultMessage: 'Old-New', + description: 'Descending date label', + } + ), + 'euiDataGridSchema.numberSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending number label', + } + ), + 'euiDataGridSchema.numberSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending number label', + } + ), + 'euiDataGridSchema.jsonSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextAsc', + { + defaultMessage: 'Small-Large', + description: 'Ascending size label', + } + ), + 'euiDataGridSchema.jsonSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextDesc', + { + defaultMessage: 'Large-Small', + description: 'Descending size label', + } + ), 'euiFilterButton.filterBadge': ({ count, hasActiveFilters }: EuiValues) => i18n.translate('core.euiFilterButton.filterBadge', { defaultMessage: '${count} ${filterCountLabel} filters', @@ -195,6 +358,19 @@ export class I18nService { 'euiHue.label': i18n.translate('core.euiHue.label', { defaultMessage: 'Select the HSV color mode "hue" value', }), + 'euiImage.closeImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.closeImage', { + defaultMessage: 'Close full screen {alt} image', + values: { alt }, + }), + 'euiImage.openImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.openImage', { + defaultMessage: 'Open full screen {alt} image', + values: { alt }, + }), + 'euiLink.external.ariaLabel': i18n.translate('core.euiLink.external.ariaLabel', { + defaultMessage: 'External link', + }), 'euiModal.closeModal': i18n.translate('core.euiModal.closeModal', { defaultMessage: 'Closes this modal window', }), @@ -217,9 +393,70 @@ export class I18nService { 'euiPopover.screenReaderAnnouncement': i18n.translate( 'core.euiPopover.screenReaderAnnouncement', { - defaultMessage: 'You are in a popup. To exit this popup, hit Escape.', + defaultMessage: 'You are in a dialog. To close this dialog, hit escape.', } ), + 'euiQuickSelect.applyButton': i18n.translate('core.euiQuickSelect.applyButton', { + defaultMessage: 'Apply', + }), + 'euiQuickSelect.fullDescription': ({ timeTense, timeValue, timeUnit }: EuiValues) => + i18n.translate('core.euiQuickSelect.fullDescription', { + defaultMessage: 'Currently set to {timeTense} {timeValue} {timeUnit}.', + values: { timeTense, timeValue, timeUnit }, + }), + 'euiQuickSelect.legendText': i18n.translate('core.euiQuickSelect.legendText', { + defaultMessage: 'Quick select a time range', + }), + 'euiQuickSelect.nextLabel': i18n.translate('core.euiQuickSelect.nextLabel', { + defaultMessage: 'Next time window', + }), + 'euiQuickSelect.previousLabel': i18n.translate('core.euiQuickSelect.previousLabel', { + defaultMessage: 'Previous time window', + }), + 'euiQuickSelect.quickSelectTitle': i18n.translate('core.euiQuickSelect.quickSelectTitle', { + defaultMessage: 'Quick select', + }), + 'euiQuickSelect.tenseLabel': i18n.translate('core.euiQuickSelect.tenseLabel', { + defaultMessage: 'Time tense', + }), + 'euiQuickSelect.unitLabel': i18n.translate('core.euiQuickSelect.unitLabel', { + defaultMessage: 'Time unit', + }), + 'euiQuickSelect.valueLabel': i18n.translate('core.euiQuickSelect.valueLabel', { + defaultMessage: 'Time value', + }), + 'euiRefreshInterval.fullDescription': ({ optionValue, optionText }: EuiValues) => + i18n.translate('core.euiRefreshInterval.fullDescription', { + defaultMessage: 'Currently set to {optionValue} {optionText}.', + values: { optionValue, optionText }, + }), + 'euiRefreshInterval.legend': i18n.translate('core.euiRefreshInterval.legend', { + defaultMessage: 'Refresh every', + }), + 'euiRefreshInterval.start': i18n.translate('core.euiRefreshInterval.start', { + defaultMessage: 'Start', + }), + 'euiRefreshInterval.stop': i18n.translate('core.euiRefreshInterval.stop', { + defaultMessage: 'Stop', + }), + 'euiRelativeTab.fullDescription': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.fullDescription', { + defaultMessage: 'The unit is changeable. Currently set to {unit}.', + values: { unit }, + }), + 'euiRelativeTab.relativeDate': ({ position }: EuiValues) => + i18n.translate('core.euiRelativeTab.relativeDate', { + defaultMessage: '{position} date', + values: { position }, + }), + 'euiRelativeTab.roundingLabel': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.roundingLabel', { + defaultMessage: 'Round to the {unit}', + values: { unit }, + }), + 'euiRelativeTab.unitInputLabel': i18n.translate('core.euiRelativeTab.unitInputLabel', { + defaultMessage: 'Relative time span', + }), 'euiSaturation.roleDescription': i18n.translate('core.euiSaturation.roleDescription', { defaultMessage: 'HSV color mode saturation and value selection', }), @@ -247,22 +484,18 @@ export class I18nService { 'euiStat.loadingText': i18n.translate('core.euiStat.loadingText', { defaultMessage: 'Statistic is loading', }), - 'euiStep.completeStep': i18n.translate('core.euiStep.completeStep', { - defaultMessage: 'Step', - description: - 'See https://elastic.github.io/eui/#/navigation/steps to know how Step control looks like', - }), - 'euiStep.incompleteStep': i18n.translate('core.euiStep.incompleteStep', { - defaultMessage: 'Incomplete Step', - }), + 'euiStep.ariaLabel': ({ status }: EuiValues) => + i18n.translate('core.euiStep.ariaLabel', { + defaultMessage: '{stepStatus}', + values: { stepStatus: status === 'incomplete' ? 'Incomplete Step' : 'Step' }, + }), 'euiStepHorizontal.buttonTitle': ({ step, title, disabled, isComplete }: EuiValues) => { return i18n.translate('core.euiStepHorizontal.buttonTitle', { - defaultMessage: - 'Step {step}: {title}{titleAppendix, select, completed { is completed} disabled { is disabled} other {}}', + defaultMessage: 'Step {step}: {title}{titleAppendix}', values: { step, title, - titleAppendix: disabled ? 'disabled' : isComplete ? 'completed' : '', + titleAppendix: disabled ? ' is disabled' : isComplete ? ' is complete' : '', }, }); }, @@ -285,6 +518,9 @@ export class I18nService { description: 'Used as the title attribute on an image or svg icon to indicate a given process step is complete', }), + 'euiStyleSelector.buttonText': i18n.translate('core.euiStyleSelector.buttonText', { + defaultMessage: 'Density', + }), 'euiSuperDatePicker.showDatesButtonLabel': i18n.translate( 'core.euiSuperDatePicker.showDatesButtonLabel', { @@ -362,6 +598,17 @@ export class I18nService { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), + 'euiTreeView.ariaLabel': ({ nodeLabel, ariaLabel }: EuiValues) => + i18n.translate('core.euiTreeView.ariaLabel', { + defaultMessage: '{nodeLabel} child of {ariaLabel}', + values: { nodeLabel, ariaLabel }, + }), + 'euiTreeView.listNavigationInstructions': i18n.translate( + 'core.euiTreeView.listNavigationInstructions', + { + defaultMessage: 'You can quickly navigate this list using arrow keys.', + } + ), }; return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f35a650cdf1b6..f83d0c9ea3c9a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -616,8 +616,6 @@ "core.euiSelectable.noAvailableOptions": "利用可能なオプションがありません", "core.euiSelectable.noMatchingOptions": "{searchValue} はどのオプションにも一致していません", "core.euiStat.loadingText": "統計を読み込み中です", - "core.euiStep.completeStep": "手順", - "core.euiStep.incompleteStep": "未完了の手順", "core.euiStepHorizontal.buttonTitle": "ステップ {step}: {title}{titleAppendix, select, completed { が完了} 無効 { が無効} other {}}", "core.euiStepHorizontal.step": "手順", "core.euiStepNumber.hasErrors": "エラーがあります", @@ -12759,4 +12757,4 @@ "xpack.licensing.check.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", "xpack.licensing.check.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fb5b7cc191e61..a830eaacd29e3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -617,8 +617,6 @@ "core.euiSelectable.noAvailableOptions": "没有任何可用选项", "core.euiSelectable.noMatchingOptions": "{searchValue} 不匹配任何选项", "core.euiStat.loadingText": "统计正在加载", - "core.euiStep.completeStep": "步骤", - "core.euiStep.incompleteStep": "未完成步骤", "core.euiStepHorizontal.buttonTitle": "第 {step} 步:{title}{titleAppendix, select, completed {已完成} disabled {已禁用} other {}}", "core.euiStepHorizontal.step": "步骤", "core.euiStepNumber.hasErrors": "有错误", @@ -12848,4 +12846,4 @@ "xpack.licensing.check.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", "xpack.licensing.check.errorUnsupportedMessage": "您的{licenseType}许可证不支持 {pluginName}。请升级您的许可证。" } -} \ No newline at end of file +} From 132f6b1e0900e22293d99409769cef4ff800a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 25 Nov 2019 10:24:09 -0500 Subject: [PATCH 08/63] Rename alertTypeParams to params (#51255) --- x-pack/legacy/plugins/alerting/README.md | 4 +- x-pack/legacy/plugins/alerting/mappings.json | 2 +- .../alerting/server/alerts_client.test.ts | 298 +++++++++--------- .../plugins/alerting/server/alerts_client.ts | 10 +- .../server/lib/task_runner_factory.test.ts | 4 +- .../server/lib/task_runner_factory.ts | 4 +- .../lib/validate_alert_type_params.test.ts | 2 +- .../server/lib/validate_alert_type_params.ts | 2 +- .../alerting/server/routes/create.test.ts | 20 +- .../plugins/alerting/server/routes/create.ts | 4 +- .../alerting/server/routes/get.test.ts | 2 +- .../alerting/server/routes/update.test.ts | 10 +- .../plugins/alerting/server/routes/update.ts | 4 +- .../legacy/plugins/alerting/server/types.ts | 4 +- .../detection_engine/alerts/create_signals.ts | 2 +- .../alerts/read_signals.test.ts | 32 +- .../detection_engine/alerts/read_signals.ts | 2 +- .../lib/detection_engine/alerts/types.ts | 2 +- .../detection_engine/alerts/update_signals.ts | 8 +- .../routes/__mocks__/request_responses.ts | 2 +- .../lib/detection_engine/routes/utils.test.ts | 10 +- .../lib/detection_engine/routes/utils.ts | 42 +-- .../common/lib/alert_utils.ts | 2 +- .../common/lib/get_test_alert_data.ts | 2 +- .../tests/alerting/alerts.ts | 10 +- .../tests/alerting/create.ts | 10 +- .../tests/alerting/find.ts | 4 +- .../security_and_spaces/tests/alerting/get.ts | 2 +- .../tests/alerting/update.ts | 16 +- .../spaces_only/tests/alerting/alerts.ts | 6 +- .../spaces_only/tests/alerting/create.ts | 2 +- .../spaces_only/tests/alerting/find.ts | 2 +- .../spaces_only/tests/alerting/get.ts | 2 +- .../spaces_only/tests/alerting/update.ts | 4 +- .../es_archives/hybrid/kibana/mappings.json | 4 +- .../es_archives/lens/basic/mappings.json | 4 +- .../es_archives/lens/reporting/mappings.json | 4 +- .../es_archives/ml/farequote/mappings.json | 2 +- .../es_archives/reporting/nanos/mappings.json | 4 +- 39 files changed, 275 insertions(+), 275 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 456eb6732c81c..40f61d11e9ace 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -202,7 +202,7 @@ Payload: |tags|A list of keywords to reference and search in the future.|string[]| |alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `DELETE /api/alert/{id}`: Delete alert @@ -246,7 +246,7 @@ Payload: |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `POST /api/alert/{id}/_enable`: Enable an alert diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index 7a1be777aff44..f840c019d5e02 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -31,7 +31,7 @@ } } }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index dc3aaaf5cf23c..08607f04a5235 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -48,7 +48,7 @@ function getMockData(overwrites: Record = {}) { alertTypeId: '123', interval: '10s', throttle: null, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -80,7 +80,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -130,25 +130,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); @@ -164,9 +164,6 @@ describe('create()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": undefined, "apiKeyOwner": undefined, "createdBy": "elastic", @@ -175,6 +172,9 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -240,7 +240,7 @@ describe('create()', () => { enabled: false, alertTypeId: '123', interval: 10000, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -263,30 +263,30 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": false, - "id": "1", - "interval": 10000, - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "enabled": false, + "id": "1", + "interval": 10000, + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); - test('should validate alertTypeParams', async () => { + test('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); const data = getMockData(); alertTypeRegistry.get.mockReturnValueOnce({ @@ -302,7 +302,7 @@ describe('create()', () => { async executor() {}, }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); @@ -337,7 +337,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -387,7 +387,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -448,7 +448,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -511,7 +511,7 @@ describe('create()', () => { ], alertTypeId: '123', name: 'abc', - alertTypeParams: { bar: true }, + params: { bar: true }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', createdBy: 'elastic', @@ -923,7 +923,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -946,24 +946,24 @@ describe('get()', () => { }); const result = await alertsClient.get({ id: '1' }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -981,7 +981,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1016,7 +1016,7 @@ describe('find()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1041,31 +1041,31 @@ describe('find()', () => { }); const result = await alertsClient.find(); expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1086,7 +1086,7 @@ describe('delete()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, scheduledTaskId: 'task-123', @@ -1155,7 +1155,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1183,7 +1183,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1198,25 +1198,25 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1233,14 +1233,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": null, "apiKeyOwner": null, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1291,7 +1291,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1320,7 +1320,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1335,26 +1335,26 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": "MTIzOmFiYw==", - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1371,14 +1371,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1400,7 +1400,7 @@ describe('update()', () => { `); }); - it('should validate alertTypeParams', async () => { + it('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); alertTypeRegistry.get.mockReturnValueOnce({ id: '123', @@ -1428,7 +1428,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1443,7 +1443,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); }); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index c260a754e4594..3916ec1d62b6c 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -77,7 +77,7 @@ interface UpdateOptions { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; }; } @@ -111,7 +111,7 @@ export class AlertsClient { public async create({ data, options }: CreateOptions) { // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const apiKey = await this.createAPIKey(); const username = await this.getUserName(); @@ -125,7 +125,7 @@ export class AlertsClient { apiKey: apiKey.created ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') : undefined, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, muteAll: false, mutedInstanceIds: [], }); @@ -199,7 +199,7 @@ export class AlertsClient { const apiKey = await this.createAPIKey(); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); this.validateActions(alertType, data.actions); const { actions, references } = this.extractReferences(data.actions); @@ -210,7 +210,7 @@ export class AlertsClient { { ...attributes, ...data, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, actions, updatedBy: username, apiKeyOwner: apiKey.created ? username : null, diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts index dcc74ed9488ce..1d91d4a35d588 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts @@ -76,7 +76,7 @@ const mockedAlertTypeSavedObject = { alertTypeId: '123', interval: '10s', mutedInstanceIds: [], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -253,7 +253,7 @@ test('validates params before executing the alert type', async () => { references: [], }); await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts index 66d445f57fe73..051b15fc8dd8f 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts @@ -94,12 +94,12 @@ export class TaskRunnerFactory { const services = getServices(fakeRequest); // Ensure API key is still valid and user has access const { - attributes: { alertTypeParams, actions, interval, throttle, muteAll, mutedInstanceIds }, + attributes: { params, actions, interval, throttle, muteAll, mutedInstanceIds }, references, } = await services.savedObjectsClient.get('alert', alertId); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, params); // Inject ids into actions const actionsWithIds = actions.map(action => { diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts index f33746798769b..e9a61354001f1 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts @@ -61,6 +61,6 @@ test('should validate and throw error when params is invalid', () => { {} ) ).toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts index 6070f2d99b605..248d896c06ac2 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -19,6 +19,6 @@ export function validateAlertTypeParams>( try { return validator.validate(params); } catch (err) { - throw Boom.badRequest(`alertTypeParams invalid: ${err.message}`); + throw Boom.badRequest(`params invalid: ${err.message}`); } } diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index c67d1a7b32352..318dbdf068d6a 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -15,7 +15,7 @@ const mockedAlert = { name: 'abc', interval: '10s', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -57,12 +57,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "id": "123", "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -83,12 +83,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -112,12 +112,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index 65fbae7c8b298..fb82a03f172b3 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -17,7 +17,7 @@ interface ScheduleRequest extends Hapi.Request { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -41,7 +41,7 @@ export const createAlertRoute = { alertTypeId: Joi.string().required(), throttle: getDurationSchema().default(null), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts index 84938a0e927d1..19618bc9e39fe 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts @@ -14,7 +14,7 @@ const mockedAlert = { id: '1', alertTypeId: '1', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index ee98f7d6dd9d3..7fc3f45911010 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -17,7 +17,7 @@ const mockedResponse = { alertTypeId: '1', tags: ['foo'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -40,7 +40,7 @@ test('calls the update function with proper parameters', async () => { name: 'abc', tags: ['bar'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -74,11 +74,11 @@ test('calls the update function with proper parameters', async () => { }, }, ], - "alertTypeParams": Object { - "otherField": false, - }, "interval": "12s", "name": "abc", + "params": Object { + "otherField": false, + }, "tags": Array [ "bar", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 9c8e0296c2f78..6aeedb93a1098 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -19,7 +19,7 @@ interface UpdateRequest extends Hapi.Request { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -43,7 +43,7 @@ export const updateAlertRoute = { .items(Joi.string()) .required(), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 359b88e21cc3b..e2460c549c05d 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -65,7 +65,7 @@ export interface Alert { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; @@ -83,7 +83,7 @@ export interface RawAlert extends SavedObjectAttributes { alertTypeId: string; interval: string; actions: RawAlertAction[]; - alertTypeParams: SavedObjectAttributes; + params: SavedObjectAttributes; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 9f472d060def7..420f995431423 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -40,7 +40,7 @@ export const createSignals = async ({ name, tags: [], alertTypeId: SIGNALS_ID, - alertTypeParams: { + params: { description, ruleId, index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts index dde3f19b1c66d..39d1fac8f7a09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts @@ -129,11 +129,11 @@ describe('read_signals', () => { test('should return a single value of rule-1 with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -150,11 +150,11 @@ describe('read_signals', () => { test('should return a single value of rule-2 with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -171,11 +171,11 @@ describe('read_signals', () => { test('should return null for a made up value with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -194,8 +194,8 @@ describe('read_signals', () => { test('returns null if the objects are not of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: 'made up 1', alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + { alertTypeId: 'made up 1', params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); @@ -205,30 +205,30 @@ describe('read_signals', () => { test('returns correct type if the objects are of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '123' } }); + expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '456' } }); + expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts index f73074b560cb2..3c49112aaf50b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts @@ -14,7 +14,7 @@ export const findSignalInArrayByRuleId = ( if (isAlertTypeArray(objects)) { const signals: SignalAlertType[] = objects; const signal: SignalAlertType[] = signals.filter(datum => { - return datum.alertTypeParams.ruleId === ruleId; + return datum.params.ruleId === ruleId; }); if (signal.length !== 0) { return signal[0]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 79e62538b1a7e..9c6e1f99c672b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -137,7 +137,7 @@ export type AlertTypeParams = Omit ({ name: 'Detect Root/Admin Users', tags: [], alertTypeId: 'siem.signals', - alertTypeParams: { + params: { description: 'Detecting root and admin users', ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 3d7f0a9fd049a..22dd7be5fbba7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -75,7 +75,7 @@ describe('utils', () => { test('should omit query if query is null', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = null; + fullSignal.params.query = null; const signal = transformAlertToSignal(fullSignal); expect(signal).toEqual({ created_by: 'elastic', @@ -105,7 +105,7 @@ describe('utils', () => { test('should omit query if query is undefined', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; + fullSignal.params.query = undefined; const signal = transformAlertToSignal(fullSignal); expect(signal).toEqual({ created_by: 'elastic', @@ -135,8 +135,8 @@ describe('utils', () => { test('should omit a mix of undefined, null, and missing fields', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; - fullSignal.alertTypeParams.language = null; + fullSignal.params.query = undefined; + fullSignal.params.language = null; const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); expect(omitData).toEqual({ created_by: 'elastic', @@ -194,7 +194,7 @@ describe('utils', () => { test('should return immutable is equal to false', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.immutable = false; + fullSignal.params.immutable = false; const signalWithEnabledFalse = transformAlertToSignal(fullSignal); expect(signalWithEnabledFalse).toEqual({ created_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index bf39d9d16b2b9..e3a677741efca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -29,32 +29,32 @@ export const getIdError = ({ export const transformAlertToSignal = (signal: SignalAlertType): Partial => { return pickBy((value: unknown) => value != null, { created_by: signal.createdBy, - description: signal.alertTypeParams.description, + description: signal.params.description, enabled: signal.enabled, - false_positives: signal.alertTypeParams.falsePositives, - filter: signal.alertTypeParams.filter, - filters: signal.alertTypeParams.filters, - from: signal.alertTypeParams.from, + false_positives: signal.params.falsePositives, + filter: signal.params.filter, + filters: signal.params.filters, + from: signal.params.from, id: signal.id, - immutable: signal.alertTypeParams.immutable, - index: signal.alertTypeParams.index, + immutable: signal.params.immutable, + index: signal.params.index, interval: signal.interval, - rule_id: signal.alertTypeParams.ruleId, - language: signal.alertTypeParams.language, - output_index: signal.alertTypeParams.outputIndex, - max_signals: signal.alertTypeParams.maxSignals, - risk_score: signal.alertTypeParams.riskScore, + rule_id: signal.params.ruleId, + language: signal.params.language, + output_index: signal.params.outputIndex, + max_signals: signal.params.maxSignals, + risk_score: signal.params.riskScore, name: signal.name, - query: signal.alertTypeParams.query, - references: signal.alertTypeParams.references, - saved_id: signal.alertTypeParams.savedId, - meta: signal.alertTypeParams.meta, - severity: signal.alertTypeParams.severity, - size: signal.alertTypeParams.size, + query: signal.params.query, + references: signal.params.references, + saved_id: signal.params.savedId, + meta: signal.params.meta, + severity: signal.params.severity, + size: signal.params.size, updated_by: signal.updatedBy, - tags: signal.alertTypeParams.tags, - to: signal.alertTypeParams.to, - type: signal.alertTypeParams.type, + tags: signal.params.tags, + to: signal.params.to, + type: signal.params.type, }); }; diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 4fbb13b229003..57b4b3b6c26c6 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -183,7 +183,7 @@ export class AlertUtils { throttle: '1m', tags: [], alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts index d7fba7e43c372..ae382652b6234 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts @@ -13,7 +13,7 @@ export function getTestAlertData(overwrites = {}) { interval: '1m', throttle: '1m', actions: [], - alertTypeParams: {}, + params: {}, ...overwrites, }; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index c43e159bbe8ca..09a642d1d14bb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -166,7 +166,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -258,7 +258,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -356,7 +356,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, @@ -491,7 +491,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', 'other'], @@ -560,7 +560,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', null, 'default'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index d94556d6cedda..bf61ee2e3f137 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -59,7 +59,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: user.username, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, @@ -173,10 +173,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['name', 'alertTypeId', 'interval', 'alertTypeParams', 'actions'], + keys: ['name', 'alertTypeId', 'interval', 'params', 'actions'], }, }); break; @@ -185,7 +185,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); - it(`should handle create alert request appropriately when alertTypeParams isn't valid`, async () => { + it(`should handle create alert request appropriately when params isn't valid`, async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') @@ -214,7 +214,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index b04c0f44e7dd4..31af7a0acffbb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -62,7 +62,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', @@ -119,7 +119,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index cfb2f34ca8056..1a8109f6b6b3c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -56,7 +56,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: response.body.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 78f70ddb13edd..1b1bcef9ad23f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -33,7 +33,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -93,7 +93,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -142,7 +142,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], throttle: '1m', alertTypeId: '1', - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -203,10 +203,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['throttle', 'name', 'tags', 'interval', 'alertTypeParams', 'actions'], + keys: ['throttle', 'name', 'tags', 'interval', 'params', 'actions'], }, }); break; @@ -222,7 +222,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.validation', - alertTypeParams: { + params: { param1: 'test', }, }) @@ -239,7 +239,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], interval: '1m', throttle: '1m', - alertTypeParams: {}, + params: {}, actions: [], }); @@ -261,7 +261,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 28634c46b6350..9af4848c57d7d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -125,7 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { getTestAlertData({ interval: '1m', alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -193,7 +193,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -238,7 +238,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 80459690af732..3018f8efffffe 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -41,7 +41,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: null, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index f49d774fc1e92..0d12af6db79b2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -45,7 +45,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: match.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index ef27a2713e98a..9e4797bcbf7ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -39,7 +39,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index 942eff0766722..a6eccf88d9e26 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -28,7 +28,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -68,7 +68,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['foo'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', diff --git a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json index 18b359d37aaa6..5256e29956f4f 100644 --- a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json @@ -99,7 +99,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1068,4 +1068,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/basic/mappings.json b/x-pack/test/functional/es_archives/lens/basic/mappings.json index b87dbe12a7005..f2a29f022ff5e 100644 --- a/x-pack/test/functional/es_archives/lens/basic/mappings.json +++ b/x-pack/test/functional/es_archives/lens/basic/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1291,4 +1291,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/reporting/mappings.json b/x-pack/test/functional/es_archives/lens/reporting/mappings.json index 0321d57bc2df6..8b8e5a0e6e7f6 100644 --- a/x-pack/test/functional/es_archives/lens/reporting/mappings.json +++ b/x-pack/test/functional/es_archives/lens/reporting/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1300,4 +1300,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/ml/farequote/mappings.json b/x-pack/test/functional/es_archives/ml/farequote/mappings.json index 4fe559cc85fe1..b00545c015a74 100644 --- a/x-pack/test/functional/es_archives/ml/farequote/mappings.json +++ b/x-pack/test/functional/es_archives/ml/farequote/mappings.json @@ -133,7 +133,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json index 34420b6bb63e1..dd717387a2643 100644 --- a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json @@ -84,7 +84,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1091,4 +1091,4 @@ } } } -} \ No newline at end of file +} From 1ce02049aa3b52f7ebfd2516c965b8aebd327c4d Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 25 Nov 2019 09:38:49 -0600 Subject: [PATCH 09/63] Swap renovate codeowners with assignee configuration (#48987) * Swap renovate codeowners with assignee configuration * remove : --- .github/CODEOWNERS | 1 - renovate.json5 | 3 +++ src/dev/renovate/config.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd73e60d1c914..4e2abd5a3db1c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,7 +45,6 @@ /x-pack/test/functional/services/transform.ts @elastic/ml-ui # Operations -/renovate.json5 @elastic/kibana-operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations diff --git a/renovate.json5 b/renovate.json5 index aefbc61e8dc12..3886715618e99 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -21,6 +21,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -28,6 +29,7 @@ major: { labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -228,6 +230,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts index 7e62059f5059a..6acbbaa4d5255 100644 --- a/src/dev/renovate/config.ts +++ b/src/dev/renovate/config.ts @@ -21,7 +21,7 @@ import { RENOVATE_PACKAGE_GROUPS } from './package_groups'; import { PACKAGE_GLOBS } from './package_globs'; import { wordRegExp, maybeFlatMap, maybeMap, getTypePackageName } from './utils'; -const DEFAULT_LABELS = ['release_note:skip', 'renovate', 'v8.0.0', 'v7.6.0']; +const DEFAULT_LABELS = ['release_note:skip', 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0']; export const RENOVATE_CONFIG = { extends: ['config:base'], From 0607032ba5b6df3341d98cf66043c3ac71d88ea7 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 25 Nov 2019 17:56:49 +0200 Subject: [PATCH 10/63] =?UTF-8?q?Move=20FilterBar=20component=20=E2=87=92?= =?UTF-8?q?=20NP=20(#51178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Moved filter bar * Fixed import * Fixed search bar test * change css import path * fix imports from data/public --- src/core/MIGRATION.md | 4 +- .../apply_filter_popover_content.tsx | 8 +++- .../public/filter/filter_bar/filter_bar.less | 0 .../core_plugins/data/public/filter/index.tsx | 2 - .../core_plugins/data/public/index.scss | 2 +- src/legacy/core_plugins/data/public/index.ts | 2 +- .../search_bar/components/search_bar.test.tsx | 7 ++- .../search_bar/components/search_bar.tsx | 3 +- src/plugins/data/public/index.ts | 2 + .../ui}/filter_bar/_global_filter_group.scss | 0 .../ui}/filter_bar/_global_filter_item.scss | 0 .../data/public/ui}/filter_bar/_index.scss | 0 .../public/ui}/filter_bar/_variables.scss | 0 .../data/public/ui}/filter_bar/filter_bar.tsx | 4 +- .../filter_editor/_filter_editor.scss | 0 .../ui}/filter_bar/filter_editor/_index.scss | 0 .../filter_editor/generic_combo_box.tsx | 0 .../ui}/filter_bar/filter_editor/index.tsx | 7 +-- .../__snapshots__/filter_label.test.js.snap | 0 .../lib/filter_editor_utils.test.ts | 43 +++++++------------ .../filter_editor/lib/filter_editor_utils.ts | 9 +--- .../filter_editor/lib/filter_label.test.js | 5 +-- .../filter_editor/lib/filter_label.tsx | 2 +- .../filter_editor/lib/filter_operators.ts | 2 +- .../filter_editor/phrase_suggestor.tsx | 11 +---- .../filter_editor/phrase_value_input.tsx | 2 +- .../filter_editor/phrases_values_input.tsx | 2 +- .../filter_editor/range_value_input.tsx | 4 +- .../filter_editor/value_input_type.tsx | 0 .../public/ui}/filter_bar/filter_item.tsx | 2 +- .../public/ui}/filter_bar/filter_options.tsx | 0 .../ui}/filter_bar/filter_view/index.tsx | 2 +- .../data/public/ui/filter_bar/index.ts | 21 +++++++++ .../data/public/ui}/index.ts | 2 +- .../legacy/plugins/ml/common/types/fields.ts | 2 +- .../transform/public/app/common/pivot_aggs.ts | 2 +- .../public/app/common/pivot_group_by.ts | 2 +- 37 files changed, 77 insertions(+), 77 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_global_filter_group.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_global_filter_item.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_index.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_variables.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_bar.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/_filter_editor.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/_index.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/generic_combo_box.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/index.tsx (99%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_editor_utils.test.ts (79%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_editor_utils.ts (93%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_label.test.js (88%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_label.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_operators.ts (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrase_suggestor.tsx (91%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrase_value_input.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrases_values_input.tsx (96%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/range_value_input.tsx (96%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/value_input_type.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_item.tsx (98%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_options.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_view/index.tsx (97%) create mode 100644 src/plugins/data/public/ui/filter_bar/index.ts rename src/{legacy/core_plugins/data/public/filter/filter_bar => plugins/data/public/ui}/index.ts (95%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f3532cd717ac8..22c96110742e0 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1167,8 +1167,8 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | -| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | +| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx index 37d96a51d66d2..954cbca8f054b 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx @@ -31,8 +31,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { IndexPattern } from '../../index_patterns'; -import { FilterLabel } from '../filter_bar/filter_editor/lib/filter_label'; -import { mapAndFlattenFilters, esFilters, utils } from '../../../../../../plugins/data/public'; +import { + mapAndFlattenFilters, + esFilters, + utils, + FilterLabel, +} from '../../../../../../plugins/data/public'; interface Props { filters: esFilters.Filter[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx index 005c4904a4f39..e48a18fc53a76 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/index.tsx @@ -17,6 +17,4 @@ * under the License. */ -export { FilterBar } from './filter_bar'; - export { ApplyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss index 14274d27c13ee..913141666c7b9 100644 --- a/src/legacy/core_plugins/data/public/index.scss +++ b/src/legacy/core_plugins/data/public/index.scss @@ -2,6 +2,6 @@ @import './query/query_bar/index'; -@import './filter/filter_bar/index'; +@import 'src/plugins/data/public/ui/filter_bar/index'; @import './search/search_bar/index'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index ffce162cadde4..b33aef75e6756 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,7 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { FilterBar, ApplyFiltersPopover } from './filter'; +export { ApplyFiltersPopover } from './filter'; export { Field, FieldType, diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 44637365247fb..0ca9482fefa30 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -35,9 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('../../../../../data/public', () => { +jest.mock('../../../../../../../plugins/data/public', () => { return { FilterBar: () =>
, + }; +}); + +jest.mock('../../../../../data/public', () => { + return { QueryBarInput: () =>
, }; }); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index c8b76c9cda99d..6a1ef77a56653 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { IndexPattern, FilterBar } from '../../../../../data/public'; +import { IndexPattern } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; import { SavedQueryMeta, SaveQueryForm } from './saved_query_management/save_query_form'; @@ -41,6 +41,7 @@ import { Query, esFilters, TimeHistoryContract, + FilterBar, } from '../../../../../../../plugins/data/public'; interface SearchBarInjectedDeps { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 6a2df6a61d136..ace0b44378b45 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -36,3 +36,5 @@ export * from './types'; export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; export * from './query'; + +export * from './ui'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_group.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_item.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss b/src/plugins/data/public/ui/filter_bar/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss rename to src/plugins/data/public/ui/filter_bar/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss rename to src/plugins/data/public/ui/filter_bar/_variables.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx rename to src/plugins/data/public/ui/filter_bar/filter_bar.tsx index e80bffb5e3c68..2f1b1f8588eb9 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -25,8 +25,8 @@ import React, { useState } from 'react'; import { FilterEditor } from './filter_editor'; import { FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; -import { IIndexPattern, esFilters } from '../../../../../../plugins/data/public'; +import { useKibana } from '../../../../kibana_react/public'; +import { IIndexPattern, esFilters } from '../..'; interface Props { filters: esFilters.Filter[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx similarity index 99% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 4f9424f30f516..12da4cbab02da 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -48,12 +48,7 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; -import { - esFilters, - utils, - IIndexPattern, - IFieldType, -} from '../../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern, IFieldType } from '../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts similarity index 79% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index 6dc9bc2300e04..2cc7f16cfe261 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -17,7 +17,6 @@ * under the License. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { existsFilter, phraseFilter, @@ -25,8 +24,8 @@ import { rangeFilter, stubIndexPattern, stubFields, -} from '../../../../../../../../plugins/data/public/stubs'; -import { IndexPattern, Field } from '../../../../index'; +} from '../../../../stubs'; +import { esFilters } from '../../../../index'; import { getFieldFromFilter, getFilterableFields, @@ -37,17 +36,12 @@ import { import { existsOperator, isBetweenOperator, isOneOfOperator, isOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; - jest.mock('ui/new_platform'); -const mockedFields = stubFields as Field[]; -const mockedIndexPattern = stubIndexPattern as IndexPattern; - describe('Filter editor utils', () => { describe('getFieldFromFilter', () => { it('should return the field from the filter', () => { - const field = getFieldFromFilter(phraseFilter, mockedIndexPattern); + const field = getFieldFromFilter(phraseFilter, stubIndexPattern); expect(field).not.toBeUndefined(); expect(field && field.name).toBe(phraseFilter.meta.key); }); @@ -117,12 +111,12 @@ describe('Filter editor utils', () => { describe('getFilterableFields', () => { it('returns the list of fields from the given index pattern', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); + const fieldOptions = getFilterableFields(stubIndexPattern); expect(fieldOptions.length).toBeGreaterThan(0); }); it('limits the fields to the filterable fields', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); + const fieldOptions = getFilterableFields(stubIndexPattern); const nonFilterableFields = fieldOptions.filter(field => !field.filterable); expect(nonFilterableFields.length).toBe(0); }); @@ -131,14 +125,14 @@ describe('Filter editor utils', () => { describe('getOperatorOptions', () => { it('returns range for number fields', () => { const [field] = stubFields.filter(({ type }) => type === 'number'); - const operatorOptions = getOperatorOptions(field as Field); + const operatorOptions = getOperatorOptions(field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).not.toBeUndefined(); }); it('does not return range for string fields', () => { const [field] = stubFields.filter(({ type }) => type === 'string'); - const operatorOptions = getOperatorOptions(field as Field); + const operatorOptions = getOperatorOptions(field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).toBeUndefined(); }); @@ -146,49 +140,44 @@ describe('Filter editor utils', () => { describe('isFilterValid', () => { it('should return false if index pattern is not provided', () => { - const isValid = isFilterValid(undefined, mockedFields[0], isOperator, 'foo'); + const isValid = isFilterValid(undefined, stubFields[0], isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if field is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, undefined, isOperator, 'foo'); + const isValid = isFilterValid(stubIndexPattern, undefined, isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if operator is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], undefined, 'foo'); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], undefined, 'foo'); expect(isValid).toBe(false); }); it('should return false for phrases filter without phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, []); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, []); expect(isValid).toBe(false); }); it('should return true for phrases filter with phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, ['foo']); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, ['foo']); expect(isValid).toBe(true); }); it('should return false for range filter without range', () => { - const isValid = isFilterValid( - mockedIndexPattern, - mockedFields[0], - isBetweenOperator, - undefined - ); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, undefined); expect(isValid).toBe(false); }); it('should return true for range filter with from', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { from: 'foo', }); expect(isValid).toBe(true); }); it('should return true for range filter with from/to', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { from: 'foo', too: 'goo', }); @@ -196,7 +185,7 @@ describe('Filter editor utils', () => { }); it('should return true for exists filter without params', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], existsOperator); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], existsOperator); expect(isValid).toBe(true); }); }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts index e4487af42beaf..422ffb162125d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -18,14 +18,9 @@ */ import dateMath from '@elastic/datemath'; -import { Ipv4Address } from '../../../../../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../../kibana_utils/public'; import { FILTER_OPERATORS, Operator } from './filter_operators'; -import { - esFilters, - IIndexPattern, - IFieldType, - isFilterable, -} from '../../../../../../../../plugins/data/public'; +import { esFilters, IIndexPattern, IFieldType, isFilterable } from '../../../..'; export function getFieldFromFilter(filter: esFilters.FieldFilter, indexPattern: IIndexPattern) { return indexPattern.fields.find(field => field.name === filter.meta.key); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js similarity index 88% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js index 0f45a33a79ebb..3eb46645522e1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js @@ -20,10 +20,7 @@ import React from 'react'; import { FilterLabel } from './filter_label'; import { shallow } from 'enzyme'; - -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import { phraseFilter } from '../../../../../../../../plugins/data/public/stubs'; -/* eslint-enable @kbn/eslint/no-restricted-paths */ +import { phraseFilter } from '../../../../stubs'; test('alias', () => { const filter = { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 1b4bdb881116b..49a0d6f2ab3bd 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -21,7 +21,7 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { existsOperator, isOneOfOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts index a3da03db71d6e..bb15cffa67b59 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; export interface Operator { message: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx similarity index 91% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 092bf8daa8f2e..61290cc16b8a8 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -19,16 +19,9 @@ import { Component } from 'react'; import { debounce } from 'lodash'; -import { - withKibana, - KibanaReactContextValue, -} from '../../../../../../../plugins/kibana_react/public'; -import { - IDataPluginServices, - IIndexPattern, - IFieldType, -} from '../../../../../../../plugins/data/public'; +import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; +import { IDataPluginServices, IIndexPattern, IFieldType } from '../../..'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx index 7ef51f88ba57e..b16994cb0057b 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx @@ -24,7 +24,7 @@ import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { value?: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx index f3b30e2ad5fd9..aa76684239b63 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx @@ -23,7 +23,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { values?: string[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx index 3c39a770377a0..65b842f0bd4aa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx @@ -21,8 +21,8 @@ import { EuiIcon, EuiLink, EuiFormHelpText, EuiFormControlLayoutDelimited } from import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React from 'react'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; -import { IFieldType } from '../../../../../../../plugins/data/public'; +import { useKibana } from '../../../../../kibana_react/public'; +import { IFieldType } from '../../..'; import { ValueInputType } from './value_input_type'; interface RangeParams { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx rename to src/plugins/data/public/ui/filter_bar/filter_item.tsx index 27406232dd5d3..4ef0b2740e5fa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import { UiSettingsClientContract } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; -import { esFilters, utils, IIndexPattern } from '../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern } from '../..'; interface Props { id: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx rename to src/plugins/data/public/ui/filter_bar/filter_options.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx rename to src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 39d4a80cdf540..dd12789d15a9d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -21,7 +21,7 @@ import { EuiBadge, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { FilterLabel } from '../filter_editor/lib/filter_label'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/plugins/data/public/ui/filter_bar/index.ts b/src/plugins/data/public/ui/filter_bar/index.ts new file mode 100644 index 0000000000000..b975317d46630 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FilterBar } from './filter_bar'; +export { FilterLabel } from './filter_editor/lib/filter_label'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts b/src/plugins/data/public/ui/index.ts similarity index 95% rename from src/legacy/core_plugins/data/public/filter/filter_bar/index.ts rename to src/plugins/data/public/ui/index.ts index 438d292b9f583..d0aaf2f6aac1c 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { FilterBar } from './filter_bar'; +export * from './filter_bar'; diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts index f9d9b6b0161e2..9e1b992eec907 100644 --- a/x-pack/legacy/plugins/ml/common/types/fields.ts +++ b/x-pack/legacy/plugins/ml/common/types/fields.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts index adc4bfd1b5918..af55732691bb0 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts index 85daad6c7fd52..e6792958ab5d2 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; From c7f8086e114d6d8bd39ab9b05f77e5c123a647cf Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 25 Nov 2019 16:02:53 +0000 Subject: [PATCH 11/63] Removes redundant code for awaiting migrations in Task Manager Thanks to #43433 being merged we no longer need to wait for the migrations to run as they are guaranteed to have run by the time plugin init has completed. This is just a cleanup making it easier to move towards the migration to the Kibana Platform. --- x-pack/legacy/plugins/task_manager/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/task_manager/index.ts b/x-pack/legacy/plugins/task_manager/index.ts index 0fda1490de714..0487b003bc1ef 100644 --- a/x-pack/legacy/plugins/task_manager/index.ts +++ b/x-pack/legacy/plugins/task_manager/index.ts @@ -72,16 +72,7 @@ export function taskManager(kibana: any) { } ); this.kbnServer.afterPluginsInit(() => { - (async () => { - // The code block below can't await directly within "afterPluginsInit" - // callback due to circular dependency. The server isn't "ready" until - // this code block finishes. Migrations wait for server to be ready before - // executing. Saved objects repository waits for migrations to finish before - // finishing the request. To avoid this, we'll await within a separate - // function block. - await this.kbnServer.server.kibanaMigrator.runMigrations(); - plugin.start(); - })(); + plugin.start(); }); server.expose(setupContract); }, From 2acc287fc2aca3f2b3552935bb988cf7899bc519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 25 Nov 2019 17:05:51 +0100 Subject: [PATCH 12/63] [Logs UI] log rate setup index validation (#50008) * Scaffold API endpoint * Implement the API endpoint * Implement API client * Set error messages in `useAnalysisSetupState` * Show validation errors next to the submit button * Check for setup errors regarding the selected indexes * Call validation only once Enrich the `availableIndices` array with validation information to show it later in the form. * Ensure validation runs before showing the indices * Adjust naming conventions - Replace `index_pattern` with `indices`, since it means something different in kibana. - Group validation actions under the `validation` namespace. * Move index error messages to the `InitialConfigurationStep` * Move error messages to the UI layer * Move validation call to `useAnalysisSetupState` * Pass timestamp as a parameter of `useAnalysisSetupState` * Fix regression with the index names in the API response * Use `_field_caps` api * s/timestamp/timestampField/g * Tweak error messages * Move `ValidationIndicesUIError` to `log_analysis_setup_state` * Track validation status It's safer to rely on the state of the promise instead of treating an empty array as "loading" * Handle network errors * Use individual `` elements for the indices This allows to disable individual checkboxes * Pass the whole `validatedIndices` array to the inner objects This will make easier to determine which indeces have errors in the checkbox list itself and simplify the state we keep track of. * Disable indices with errors Show a tooltip above the disabled index to explain why it cannot be selected. * Pass indices to the API as an array * Show overlay while the validation loads * Wrap tooltips on a `block` element Prevents the checkboxes from collapsing on the same line * Use the right dependencies for `useEffect => validateIndices()` * Restore formatter function name * Simplify mapping of selected indices to errors * s/checked/isSelected/g * Make errors field-generic * Allow multiple errors per index * Simplify code a bit --- .../common/http_api/log_analysis/index.ts | 1 + .../http_api/log_analysis/validation/index.ts | 7 + .../log_analysis/validation/indices.ts | 51 ++++++ x-pack/legacy/plugins/infra/package.json | 2 +- .../components/loading_overlay_wrapper.tsx | 1 + .../api/index_patterns_validate.ts | 33 ++++ .../logs/log_analysis/log_analysis_jobs.tsx | 5 +- .../log_analysis/log_analysis_setup_state.tsx | 107 ++++++++++--- .../pages/logs/analysis/page_content.tsx | 2 + .../logs/analysis/page_setup_content.tsx | 3 + .../analysis_setup_indices_form.tsx | 148 ++++++++++++------ .../initial_configuration_step.tsx | 92 ++++++++--- .../setup/process_step/process_step.tsx | 4 +- .../pages/logs/analysis/setup/setup_steps.tsx | 13 +- .../plugins/infra/server/infra_server.ts | 6 +- .../lib/adapters/framework/adapter_types.ts | 25 ++- .../infra/server/routes/log_analysis/index.ts | 1 + .../log_analysis/index_patterns/index.ts | 7 + .../log_analysis/index_patterns/validate.ts | 83 ++++++++++ 19 files changed, 487 insertions(+), 104 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts index 38684cb22e237..378e32cb3582c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './validation'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts new file mode 100644 index 0000000000000..727faca69298e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './indices'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts new file mode 100644 index 0000000000000..62d81dc136853 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices'; + +/** + * Request types + */ +export const validationIndicesRequestPayloadRT = rt.type({ + data: rt.type({ + timestampField: rt.string, + indices: rt.array(rt.string), + }), +}); + +export type ValidationIndicesRequestPayload = rt.TypeOf; + +/** + * Response types + * */ +export const validationIndicesErrorRT = rt.union([ + rt.type({ + error: rt.literal('INDEX_NOT_FOUND'), + index: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_FOUND'), + index: rt.string, + field: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_VALID'), + index: rt.string, + field: rt.string, + }), +]); + +export type ValidationIndicesError = rt.TypeOf; + +export const validationIndicesResponsePayloadRT = rt.type({ + data: rt.type({ + errors: rt.array(validationIndicesErrorRT), + }), +}); + +export type ValidationIndicesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/package.json b/x-pack/legacy/plugins/infra/package.json index 63812bb2da513..7aa8cb9b5269a 100644 --- a/x-pack/legacy/plugins/infra/package.json +++ b/x-pack/legacy/plugins/infra/package.json @@ -16,4 +16,4 @@ "boom": "7.3.0", "lodash": "^4.17.15" } -} \ No newline at end of file +} diff --git a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx index a99b265fc3ea9..5df1fc07e83b9 100644 --- a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx +++ b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx @@ -40,4 +40,5 @@ const OverlayDiv = euiStyled.div` position: absolute; top: 0; width: 100%; + z-index: ${props => props.theme.eui.euiZLevel1}; `; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts new file mode 100644 index 0000000000000..440ee10e4223d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { kfetch } from 'ui/kfetch'; + +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, +} from '../../../../../common/http_api'; + +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; + +export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => { + const response = await kfetch({ + method: 'POST', + pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + body: JSON.stringify( + validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } }) + ), + }); + + return pipe( + validationIndicesResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx index 163f0e39d1228..0f386f416b866 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -94,8 +94,8 @@ export const useLogAnalysisJobs = ({ dispatch({ type: 'fetchingJobStatuses' }); return await callJobsSummaryAPI(spaceId, sourceId); }, - onResolve: response => { - dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId }); + onResolve: jobResponse => { + dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId }); }, onReject: err => { dispatch({ type: 'failedFetchingJobStatuses' }); @@ -158,6 +158,7 @@ export const useLogAnalysisJobs = ({ setup: setupMlModule, setupMlModuleRequest, setupStatus: statusState.setupStatus, + timestampField: timeField, viewSetupForReconfiguration, viewSetupForUpdate, viewResults, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 7942657018455..c965c50bedccc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { isExampleDataIndex } from '../../../../common/log_analysis'; +import { + ValidationIndicesError, + ValidationIndicesResponsePayload, +} from '../../../../common/http_api'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callIndexPatternsValidate } from './api/index_patterns_validate'; type SetupHandler = ( indices: string[], @@ -14,42 +20,75 @@ type SetupHandler = ( endTime: number | undefined ) => void; +export type ValidationIndicesUIError = + | ValidationIndicesError + | { error: 'NETWORK_ERROR' } + | { error: 'TOO_FEW_SELECTED_INDICES' }; + +export interface ValidatedIndex { + index: string; + errors: ValidationIndicesError[]; + isSelected: boolean; +} + interface AnalysisSetupStateArguments { availableIndices: string[]; cleanupAndSetupModule: SetupHandler; setupModule: SetupHandler; + timestampField: string; } -type IndicesSelection = Record; - -type ValidationErrors = 'TOO_FEW_SELECTED_INDICES'; - const fourWeeksInMs = 86400000 * 7 * 4; export const useAnalysisSetupState = ({ availableIndices, cleanupAndSetupModule, setupModule, + timestampField, }: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); - const [selectedIndices, setSelectedIndices] = useState( - availableIndices.reduce( - (indexMap, indexName) => ({ - ...indexMap, - [indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)), - }), - {} - ) + // Prepare the validation + const [validatedIndices, setValidatedIndices] = useState( + availableIndices.map(index => ({ + index, + errors: [], + isSelected: false, + })) + ); + const [validateIndicesRequest, validateIndices] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callIndexPatternsValidate(timestampField, availableIndices); + }, + onResolve: ({ data }: ValidationIndicesResponsePayload) => { + setValidatedIndices( + availableIndices.map(index => { + const errors = data.errors.filter(error => error.index === index); + return { + index, + errors, + isSelected: errors.length === 0 && !isExampleDataIndex(index), + }; + }) + ); + }, + onReject: () => { + setValidatedIndices([]); + }, + }, + [availableIndices, timestampField] ); + useEffect(() => { + validateIndices(); + }, [validateIndices]); + const selectedIndexNames = useMemo( - () => - Object.entries(selectedIndices) - .filter(([_indexName, isSelected]) => isSelected) - .map(([indexName]) => indexName), - [selectedIndices] + () => validatedIndices.filter(i => i.isSelected).map(i => i.index), + [validatedIndices] ); const setup = useCallback(() => { @@ -60,24 +99,42 @@ export const useAnalysisSetupState = ({ return cleanupAndSetupModule(selectedIndexNames, startTime, endTime); }, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]); - const validationErrors: ValidationErrors[] = useMemo( + const isValidating = useMemo( () => - Object.values(selectedIndices).some(isSelected => isSelected) - ? [] - : ['TOO_FEW_SELECTED_INDICES' as const], - [selectedIndices] + validateIndicesRequest.state === 'pending' || + validateIndicesRequest.state === 'uninitialized', + [validateIndicesRequest.state] ); + const validationErrors = useMemo(() => { + if (isValidating) { + return []; + } + + if (validateIndicesRequest.state === 'rejected') { + return [{ error: 'NETWORK_ERROR' }]; + } + + if (selectedIndexNames.length === 0) { + return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; + } + + return validatedIndices.reduce((errors, index) => { + return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors; + }, []); + }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + return { cleanupAndSetup, endTime, + isValidating, selectedIndexNames, - selectedIndices, setEndTime, - setSelectedIndices, setStartTime, setup, startTime, + validatedIndices, + setValidatedIndices, validationErrors, }; }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx index 04d7520c0ca88..f0a26eae25ecb 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx @@ -27,6 +27,7 @@ export const AnalysisPageContent = () => { lastSetupErrorMessages, setup, setupStatus, + timestampField, viewResults, } = useContext(LogAnalysisJobs.Context); @@ -61,6 +62,7 @@ export const AnalysisPageContent = () => { errorMessages={lastSetupErrorMessages} setup={setup} setupStatus={setupStatus} + timestampField={timestampField} viewResults={viewResults} /> ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx index 097cccf5dca33..7ae174c4a7899 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx @@ -34,6 +34,7 @@ interface AnalysisSetupContentProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -43,6 +44,7 @@ export const AnalysisSetupContent: React.FunctionComponent { useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); @@ -82,6 +84,7 @@ export const AnalysisSetupContent: React.FunctionComponent diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx index defcefd69a7ab..585a65b9ad1c8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -4,37 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCheckboxGroup, EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; - -export type IndicesSelection = Record; - -export type IndicesValidationError = 'TOO_FEW_SELECTED_INDICES'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; +import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ - indices: IndicesSelection; - onChangeSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; -}> = ({ indices, onChangeSelectedIndices, validationErrors = [] }) => { + indices: ValidatedIndex[]; + isValidating: boolean; + onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void; + valid: boolean; +}> = ({ indices, isValidating, onChangeSelectedIndices, valid }) => { + const handleCheckboxChange = useCallback( + (event: React.ChangeEvent) => { + onChangeSelectedIndices( + indices.map(index => { + const checkbox = event.currentTarget; + return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + }) + ); + }, + [indices, onChangeSelectedIndices] + ); + const choices = useMemo( () => - Object.keys(indices).map(indexName => ({ - id: indexName, - label: {indexName}, - })), - [indices] - ); + indices.map(index => { + const validIndex = index.errors.length === 0; + const checkbox = ( + {index.index}} + onChange={handleCheckboxChange} + checked={index.isSelected} + disabled={!validIndex} + /> + ); - const handleCheckboxGroupChange = useCallback( - indexName => { - onChangeSelectedIndices({ - ...indices, - [indexName]: !indices[indexName], - }); - }, - [indices, onChangeSelectedIndices] + return validIndex ? ( + checkbox + ) : ( +
+ {checkbox} +
+ ); + }), + [indices] ); return ( @@ -53,20 +74,17 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ /> } > - 0} - label={indicesSelectionLabel} - labelType="legend" - > - - + + + <>{choices} + + ); }; @@ -75,14 +93,50 @@ const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesS defaultMessage: 'Indices', }); -const formatValidationError = (validationError: IndicesValidationError) => { - switch (validationError) { - case 'TOO_FEW_SELECTED_INDICES': - return i18n.translate( - 'xpack.infra.analysisSetup.indicesSelectionTooFewSelectedIndicesDescription', - { - defaultMessage: 'Select at least one index name.', - } - ); - } +const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { + return errors.map(error => { + switch (error.error) { + case 'INDEX_NOT_FOUND': + return ( +

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

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

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

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

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

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

+

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

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

{{screenTitle}}

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

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

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

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

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

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

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 9eed3a59acaa6..ccf14879baa37 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -298,6 +298,35 @@ class Plugin { } } ``` +If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` +as a temporary solution until error migration is complete: +```ts +// legacy/plugins/demoplugin/server/plugin.ts +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'src/core/server'; + +export interface DemoPluginsSetup {}; + +class Plugin { + public setup(core: CoreSetup, pluginSetup: DemoPluginSetup) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/demoplugin/search', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + router.wrapErrors((context, req, res) => { + throw Boom.notFound('not there'); // will be converted into proper New Platform error + }) + ) + } +} +``` + #### 4. New Platform plugin As the final step we delete the shim and move all our code into a New Platform diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 00c9aedc42cfb..e9a2571382edc 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,6 +45,7 @@ const createRouterMock = (): jest.Mocked => ({ put: jest.fn(), delete: jest.fn(), getRoutes: jest.fn(), + handleLegacyErrors: jest.fn().mockImplementation(handler => handler), }); const createSetupContractMock = () => { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index ff1ff3acfae3d..2fa67750f6406 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -45,6 +45,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 70d7ae00f917e..481d8e1bbf49b 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -164,6 +164,53 @@ describe('Handler', () => { }); }); +describe('handleLegacyErrors', () => { + it('properly convert Boom errors', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false }, + router.handleLegacyErrors((context, req, res) => { + throw Boom.notFound(); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('Not Found'); + }); + + it('returns default error when non-Boom errors are thrown', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { + path: '/', + validate: false, + }, + router.handleLegacyErrors((context, req, res) => { + throw new Error('Unexpected'); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, + }); + }); +}); + describe('Response factory', () => { describe('Success', () => { it('supports answering with json object', async () => { diff --git a/src/core/server/http/router/error_wrapper.test.ts b/src/core/server/http/router/error_wrapper.test.ts new file mode 100644 index 0000000000000..aa20b49dc9c91 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.test.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response'; +import { wrapErrors } from './error_wrapper'; +import { KibanaRequest, RequestHandler, RequestHandlerContext } from 'kibana/server'; + +const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); +}; + +describe('wrapErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = wrapErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBeInstanceOf(KibanaResponse); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = wrapErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); +}); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts new file mode 100644 index 0000000000000..706a9fe3b8887 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { ObjectType, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest } from './request'; +import { KibanaResponseFactory } from './response'; +import { RequestHandler } from './router'; +import { RequestHandlerContext } from '../../../server'; + +export const wrapErrors =

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ +

+
+ +

+ +

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

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

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

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

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

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

+ + ), + }} + /> +

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

+ + ), + }} + /> +

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

- -

-
- - - -
- - ); -}; diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 6f339a6fc9c95..ff2db01cb6c58 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -25,6 +25,7 @@ const setupHttp = (basePath: string) => { }); return http; }; +const tenant = ''; afterEach(() => { fetchMock.restore(); @@ -32,7 +33,7 @@ afterEach(() => { it(`logs out 401 responses`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const logoutPromise = new Promise(resolve => { jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve()); }); @@ -58,7 +59,7 @@ it(`ignores anonymous paths`, async () => { const http = setupHttp('/foo'); const { anonymousPaths } = http; anonymousPaths.register('/bar'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); @@ -69,7 +70,7 @@ it(`ignores anonymous paths`, async () => { it(`ignores errors which don't have a response, for example network connectivity issues`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); @@ -80,7 +81,7 @@ it(`ignores errors which don't have a response, for example network connectivity it(`ignores requests which omit credentials`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts new file mode 100644 index 0000000000000..e9c4b6e281cf3 --- /dev/null +++ b/x-pack/plugins/security/public/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: string; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 78c6feac0fa29..12b4620d554a2 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,11 @@ function getMockOptions(config: Partial = {}) { basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), isSystemAPIRequest: jest.fn(), - config: { sessionTimeout: null, authc: { providers: [], oidc: {}, saml: {} }, ...config }, + config: { + session: { idleTimeout: null, lifespan: null }, + authc: { providers: [], oidc: {}, saml: {} }, + ...config, + }, sessionStorageFactory: sessionStorageMock.createFactory(), }; } @@ -51,7 +55,9 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ authc: { providers: [], oidc: {}, saml: {} } }); + const mockOptions = getMockOptions({ + authc: { providers: [], oidc: {}, saml: {} }, + }); expect(() => new Authenticator(mockOptions)).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); @@ -73,7 +79,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -151,7 +159,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -173,7 +182,12 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.login(request, { provider: 'basic', @@ -286,7 +300,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -344,7 +360,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -366,7 +383,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -381,7 +399,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -400,7 +423,58 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + + it('properly extends session expiration if it is defined.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + + // Create new authenticator with non-null session `idleTimeout`. + mockOptions = getMockOptions({ + session: { + idleTimeout: 3600 * 24, + lifespan: null, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -408,27 +482,39 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: currentDate + 3600 * 24, + lifespanExpiration: null, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('properly extends session timeout if it is defined.', async () => { + it('does not extend session lifespan expiration.', async () => { const user = mockAuthenticatedUser(); const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const hr = 1000 * 60 * 60; - // Create new authenticator with non-null `sessionTimeout`. + // Create new authenticator with non-null session `idleTimeout` and `lifespan`. mockOptions = getMockOptions({ - sessionTimeout: 3600 * 24, + session: { + idleTimeout: hr * 2, + lifespan: hr * 8, + }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) + // it was last extended 1 hour ago, which means it will expire in 1 hour + idleTimeoutExpiration: currentDate + hr * 1, + lifespanExpiration: currentDate + hr * 1.5, + state, + provider: 'basic', + }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); authenticator = new Authenticator(mockOptions); @@ -445,13 +531,69 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: currentDate + 3600 * 24, + idleTimeoutExpiration: currentDate + hr * 2, + lifespanExpiration: currentDate + hr * 1.5, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); + it('only updates the session lifespan expiration if it does not match the current server config.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const hr = 1000 * 60 * 60; + + async function createAndUpdateSession( + lifespan: number | null, + oldExpiration: number | null, + newExpiration: number | null + ) { + mockOptions = getMockOptions({ + session: { + idleTimeout: null, + lifespan, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: 1, + lifespanExpiration: oldExpiration, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: 1, + lifespanExpiration: newExpiration, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + } + // do not change max expiration + createAndUpdateSession(hr * 8, 1234, 1234); + createAndUpdateSession(null, null, null); + // change max expiration + createAndUpdateSession(null, 1234, null); + createAndUpdateSession(hr * 8, null, hr * 8); + }); + it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); @@ -460,7 +602,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -477,7 +624,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -497,7 +649,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -508,7 +661,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -526,7 +680,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -537,7 +692,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -552,7 +708,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -569,7 +730,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -585,7 +751,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.redirectTo('some-url', { state: null }) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); @@ -602,7 +773,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -619,7 +795,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -636,7 +817,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -653,7 +839,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -668,7 +859,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -697,7 +890,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.logout.mockResolvedValue( DeauthenticationResult.redirectTo('some-url') ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const deauthenticationResult = await authenticator.logout(request); @@ -707,10 +905,41 @@ describe('Authenticator', () => { expect(deauthenticationResult.redirectURL).toBe('some-url'); }); + it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic' } }); + mockSessionStorage.get.mockResolvedValue(null); + + mockBasicAuthenticationProvider.logout.mockResolvedValue( + DeauthenticationResult.redirectTo('some-url') + ); + + const deauthenticationResult = await authenticator.logout(request); + + expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + expect(deauthenticationResult.redirected()).toBe(true); + expect(deauthenticationResult.redirectURL).toBe('some-url'); + }); + + it('returns `notHandled` if session does not exist and provider name is invalid', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } }); + mockSessionStorage.get.mockResolvedValue(null); + + const deauthenticationResult = await authenticator.logout(request); + + expect(deauthenticationResult.notHandled()).toBe(true); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const deauthenticationResult = await authenticator.logout(request); @@ -719,4 +948,51 @@ describe('Authenticator', () => { expect(deauthenticationResult.notHandled()).toBe(true); }); }); + + describe('`getSessionInfo` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType; + let mockSessionStorage: jest.Mocked>; + beforeEach(() => { + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + }); + + it('returns current session info if session exists.', async () => { + const request = httpServerMock.createKibanaRequest(); + const state = { authorization: 'Basic xxx' }; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const mockInfo = { + now: currentDate, + idleTimeoutExpiration: currentDate + 60000, + lifespanExpiration: currentDate + 120000, + provider: 'basic', + }; + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, + lifespanExpiration: mockInfo.lifespanExpiration, + state, + provider: mockInfo.provider, + }); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toEqual(mockInfo); + }); + + it('returns `null` if session does not exist.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 18bdc9624b12b..17a773c6b6e8c 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -31,6 +31,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; +import { SessionInfo } from '../../public/types'; /** * The shape of the session that is actually stored in the cookie. @@ -45,7 +46,13 @@ export interface ProviderSession { * The Unix time in ms when the session should be considered expired. If `null`, session will stay * active until the browser is closed. */ - expires: number | null; + idleTimeoutExpiration: number | null; + + /** + * The Unix time in ms which is the max total lifespan of the session. If `null`, session expire + * time can be extended indefinitely. + */ + lifespanExpiration: number | null; /** * Session value that is fed to the authentication provider. The shape is unknown upfront and @@ -77,7 +84,7 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - config: Pick; + config: Pick; basePath: HttpServiceSetup['basePath']; loggers: LoggerFactory; clusterClient: IClusterClient; @@ -153,9 +160,14 @@ export class Authenticator { private readonly providers: Map; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. + * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly ttl: number | null = null; + private readonly idleTimeout: number | null = null; + + /** + * Session max lifespan in ms. If `null` session may live indefinitely. + */ + private readonly lifespan: number | null = null; /** * Internal authenticator logger. @@ -202,7 +214,9 @@ export class Authenticator { }) ); - this.ttl = this.options.config.sessionTimeout; + // only set these vars if they are defined in options (otherwise coalesce to existing/default) + this.idleTimeout = this.options.config.session.idleTimeout; + this.lifespan = this.options.config.session.lifespan; } /** @@ -257,10 +271,12 @@ export class Authenticator { if (existingSession && shouldClearSession) { sessionStorage.clear(); } else if (!attempt.stateless && authenticationResult.shouldUpdateState()) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.state, provider: attempt.provider, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } @@ -315,10 +331,18 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const sessionValue = await this.getSessionValue(sessionStorage); + const providerName = this.getProviderName(request.query); if (sessionValue) { sessionStorage.clear(); return this.providers.get(sessionValue.provider)!.logout(request, sessionValue.state); + } else if (providerName) { + // provider name is passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it + const provider = this.providers.get(providerName); + if (provider) { + return provider.logout(request, null); + } } // Normally when there is no active session in Kibana, `logout` method shouldn't do anything @@ -334,6 +358,29 @@ export class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Returns session information for the current request. + * @param request Request instance. + */ + async getSessionInfo(request: KibanaRequest): Promise { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const sessionValue = await this.getSessionValue(sessionStorage); + + if (sessionValue) { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + return { + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + }; + } + return null; + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. @@ -410,13 +457,34 @@ export class Authenticator { ) { sessionStorage.clear(); } else if (sessionCanBeUpdated) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, provider: providerType, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } } + + private getProviderName(query: any): string | null { + if (query && query.provider && typeof query.provider === 'string') { + return query.provider; + } + return null; + } + + private calculateExpiry( + existingSession: ProviderSession | null + ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { + let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; + if (existingSession && existingSession.lifespanExpiration && this.lifespan) { + lifespanExpiration = existingSession.lifespanExpiration; + } + const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + + return { idleTimeoutExpiration, lifespanExpiration }; + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index dcaf26f53fe01..77f1f9e45aea7 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -14,5 +14,6 @@ export const authenticationMock = { invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), logout: jest.fn(), + getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index df16dd375e858..2e67a0eaaa6d5 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -68,15 +68,22 @@ export async function setupAuthentication({ const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, - config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, + config: { session: config.session, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, - validate: (sessionValue: ProviderSession) => - !(sessionValue.expires && sessionValue.expires < Date.now()), + validate: (sessionValue: ProviderSession) => { + const { idleTimeoutExpiration, lifespanExpiration } = sessionValue; + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; + }, }), }); @@ -151,6 +158,7 @@ export async function setupAuthentication({ return { login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), + getSessionInfo: authenticator.getSessionInfo.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 8eb20447c7e2c..a6850dcdf8321 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -422,20 +422,16 @@ describe('TokenAuthenticationProvider', () => { }); describe('`logout` method', () => { - it('returns `notHandled` if state is not presented.', async () => { + it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); sinon.assert.notCalled(mockOptions.tokens.invalidate); - - deauthenticateResult = await provider.logout(request, tokenPair); - expect(deauthenticateResult.notHandled()).toBe(false); }); it('fails if `tokens.invalidate` fails', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index d1881ad4b5498..c5f8f07e50b11 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -120,18 +120,16 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async logout(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to log user out via ${request.url.path}.`); - if (!state) { + if (state) { + this.logger.debug('Token-based logout has been initiated by the user.'); + try { + await this.options.tokens.invalidate(state); + } catch (err) { + this.logger.debug(`Failed invalidating user's access token: ${err.message}`); + return DeauthenticationResult.failed(err); + } + } else { this.logger.debug('There are no access and refresh tokens to invalidate.'); - return DeauthenticationResult.notHandled(); - } - - this.logger.debug('Token-based logout has been initiated by the user.'); - - try { - await this.options.tokens.invalidate(state); - } catch (err) { - this.logger.debug(`Failed invalidating user's access token: ${err.message}`); - return DeauthenticationResult.failed(err); } const queryString = request.url.search || `?msg=LOGGED_OUT`; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 569611516c880..9ddb3e6e96b90 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,48 +13,57 @@ import { createConfig$, ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { @@ -253,7 +262,11 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16), secureCookies: true }); + expect(config).toEqual({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + session: { idleTimeout: null, lifespan: null }, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -273,7 +286,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: false }); + expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -293,7 +306,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -313,7 +326,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index a257a25344393..c7d990f81369e 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -34,7 +34,11 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + session: schema.object({ + idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ providers: schema.arrayOf(schema.string(), { defaultValue: ['basic'], minSize: 1 }), @@ -83,11 +87,23 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b secureCookies = true; } - return { + // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" + // however, NP does not yet have a mechanism to automatically rename deprecated keys + // for the time being, we'll do it manually: + const sess = config.session; + const session = { + idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, + lifespan: (sess && sess.lifespan) || null, + }; + + const val = { ...config, encryptionKey, secureCookies, + session, }; + delete val.sessionTimeout; // DEPRECATED + return val; }) ); } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 2ff0e915fc1b0..26788c3ef9230 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -20,7 +20,10 @@ describe('Security Plugin', () => { plugin = new Plugin( coreMock.createPluginInitializerContext({ cookieName: 'sid', - sessionTimeout: 1500, + session: { + idleTimeout: 1500, + lifespan: null, + }, authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, @@ -54,7 +57,10 @@ describe('Security Plugin', () => { "cookieName": "sid", "loginAssistanceMessage": undefined, "secureCookies": true, - "sessionTimeout": 1500, + "session": Object { + "idleTimeout": 1500, + "lifespan": null, + }, }, "license": Object { "getFeatures": [Function], @@ -66,6 +72,7 @@ describe('Security Plugin', () => { "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], + "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], "login": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index c8761050524a5..e956603517349 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -74,7 +74,10 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - sessionTimeout: number | null; + session: { + idleTimeout: number | null; + lifespan: number | null; + }; secureCookies: boolean; authc: { providers: string[] }; }>; @@ -206,7 +209,10 @@ export class Plugin { // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { loginAssistanceMessage: config.loginAssistanceMessage, - sessionTimeout: config.sessionTimeout, + session: { + idleTimeout: config.session.idleTimeout, + lifespan: config.session.lifespan, + }, secureCookies: config.secureCookies, cookieName: config.cookieName, authc: { providers: config.authc.providers }, diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 0e3f03255dcb9..086647dcb3459 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; import { RouteDefinitionParams } from '..'; export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + defineSessionRoutes(params); if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authentication/session.ts b/x-pack/plugins/security/server/routes/authentication/session.ts new file mode 100644 index 0000000000000..cdebc19d7cf8d --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/session.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for all authentication realms. + */ +export function defineSessionRoutes({ router, logger, authc, basePath }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, request, response) => { + try { + const sessionInfo = await authc.getSessionInfo(request); + // This is an authenticated request, so sessionInfo will always be non-null. + return response.ok({ body: sessionInfo! }); + } catch (err) { + logger.error(`Error retrieving user session: ${err.message}`); + return response.internalError(); + } + } + ); + + router.post( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, _request, response) => { + // We can't easily return updated session info in a single HTTP call, because session data is obtained from + // the HTTP request, not the response. So the easiest way to facilitate this is to redirect the client to GET + // the session endpoint after the client's session has been extended. + return response.redirected({ + headers: { + location: `${basePath.serverBasePath}/internal/security/session`, + }, + }); + } + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f83d0c9ea3c9a..f5fc453557122 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9929,9 +9929,8 @@ "xpack.security.account.passwordsDoNotMatch": "パスワードが一致していません。", "xpack.security.account.usernameGroupDescription": "この情報は変更できません。", "xpack.security.account.usernameGroupTitle": "ユーザー名とメールアドレス", - "xpack.security.components.sessionTimeoutWarning.message": "操作がないため間もなくログアウトします。再開するには [OK] をクリックしてくださ。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "ログイン", "xpack.security.loggedOut.title": "ログアウト完了", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワード再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a830eaacd29e3..288fc92be3cbd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10018,9 +10018,8 @@ "xpack.security.account.passwordsDoNotMatch": "密码不匹配。", "xpack.security.account.usernameGroupDescription": "不能更改此信息。", "xpack.security.account.usernameGroupTitle": "用户名和电子邮件", - "xpack.security.components.sessionTimeoutWarning.message": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "登录", "xpack.security.loggedOut.title": "已成功退出", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。", diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 4d034622427fc..052d984774e69 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./session')); }); } diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts new file mode 100644 index 0000000000000..7c7883f58cb30 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Cookie, cookie } from 'request'; +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + describe('Session', () => { + let sessionCookie: Cookie; + + const saveCookie = async (response: any) => { + // save the response cookie, and pass back the result + sessionCookie = cookie(response.headers['set-cookie'][0])!; + return response; + }; + const getSessionInfo = async () => + supertest + .get('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-api', 'true') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(200); + const extendSession = async () => + supertest + .post('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(302) + .then(saveCookie); + + beforeEach(async () => { + await supertest + .post('/api/security/v1/login') + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204) + .then(saveCookie); + }); + + describe('GET /internal/security/session', () => { + it('should return current session information', async () => { + const { body } = await getSessionInfo(); + expect(body.now).to.be.a('number'); + expect(body.idleTimeoutExpiration).to.be.a('number'); + expect(body.lifespanExpiration).to.be(null); + expect(body.provider).to.be('basic'); + }); + + it('should not extend the session', async () => { + const { body } = await getSessionInfo(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + }); + }); + + describe('POST /internal/security/session', () => { + it('should redirect to GET', async () => { + const response = await extendSession(); + expect(response.headers.location).to.be('/internal/security/session'); + }); + + it('should extend the session', async () => { + // browsers will follow the redirect and return the new session info, but this testing framework does not + // we simulate that behavior in this test by sending another GET request + const { body } = await getSessionInfo(); + await extendSession(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 64a9cafca406a..9c67dfe61b957 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -21,6 +21,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', ], }, diff --git a/yarn.lock b/yarn.lock index e30abf76145a3..7e965979fd46f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1004,7 +1004,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -6448,6 +6448,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + big-time@2.x.x: version "2.0.1" resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" @@ -6779,6 +6784,19 @@ brfs@^2.0.2: static-module "^3.0.2" through2 "^2.0.0" +broadcast-channel@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.0.3.tgz#e6668693af410f7dda007fd6f80e21992d51f3cc" + integrity sha512-ogRIiGDL0bdeOzPO13YQKX12IvRBDOxej2CJaEwuEOF011C9JBABz+8MJ/WZ34eGbXGrfVBeeeaMTWjBzxVKkw== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "3.0.0" + unload "2.2.0" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -16938,6 +16956,11 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" integrity sha512-/812MXr9RBtMObviZ8gQBhHO8MOrGj8HlEE+4ccMTElNA/6I3u39u+bhny55Lk921yn44nSZFy9naNLElL5wgQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -18997,6 +19020,11 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha1-R9x7z2IXG4Aw4hUv2C8SpolKcRk= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -19577,6 +19605,13 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -24453,6 +24488,13 @@ rimraf@2.6.3, rimraf@^2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.0, rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -24460,13 +24502,6 @@ rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" - integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== - dependencies: - glob "^7.1.3" - rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -28185,6 +28220,14 @@ unlazy-loader@^0.1.3: dependencies: requires-regex "^0.3.3" +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 1fb779b7d03a4bc970f1d987080b524d23fdec52 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 26 Nov 2019 11:34:03 -0500 Subject: [PATCH 50/63] Skipped these tests because their apps are not enabled on cloud. (#51677) --- x-pack/test/functional/apps/cross_cluster_replication/index.ts | 2 +- x-pack/test/functional/apps/remote_clusters/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 21fc1982edc42..efcfaaba6037c 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index dc47bd9de3815..9a4bc5b6a5cbd 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; From 074f24ee32c5ef59c49eb89856e0502eb15f3d75 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 26 Nov 2019 18:03:16 +0100 Subject: [PATCH 51/63] [APM] Fix watcher integration (#51721) Closes #51720. --- .../ServiceIntegrations/WatcherFlyout.tsx | 1 + .../__test__/createErrorGroupWatch.test.ts | 20 +++++++++++++------ .../createErrorGroupWatch.ts | 10 ++++++++-- .../apm/public/services/rest/callApi.ts | 7 +++---- .../services/rest/{watcher.js => watcher.ts} | 15 +++++++++++--- 5 files changed, 38 insertions(+), 15 deletions(-) rename x-pack/legacy/plugins/apm/public/services/rest/{watcher.js => watcher.ts} (60%) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index d52c869b95872..18964531958f7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -191,6 +191,7 @@ export class WatcherFlyout extends Component< ) as string; return createErrorGroupWatch({ + http: core.http, emails, schedule, serviceName, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index c7860b81a7b1e..f05d343ad7ba5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -7,22 +7,30 @@ import { isArray, isObject, isString } from 'lodash'; import mustache from 'mustache'; import uuid from 'uuid'; -// @ts-ignore import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; +import { HttpServiceBase } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; +jest.mock('../../../../../services/rest/callApi', () => ({ + callApi: () => Promise.resolve(null) +})); + describe('createErrorGroupWatch', () => { let createWatchResponse: string; let tmpl: any; + const createWatchSpy = jest + .spyOn(rest, 'createWatch') + .mockResolvedValue(undefined); + beforeEach(async () => { jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); - jest.spyOn(rest, 'createWatch').mockReturnValue(undefined); createWatchResponse = await createErrorGroupWatch({ + http: {} as HttpServiceBase, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { @@ -36,19 +44,19 @@ describe('createErrorGroupWatch', () => { apmIndexPatternTitle: 'myIndexPattern' }); - const watchBody = rest.createWatch.mock.calls[0][1]; + const watchBody = createWatchSpy.mock.calls[0][0].watch; const templateCtx = { payload: esResponse, metadata: watchBody.metadata }; - tmpl = renderMustache(rest.createWatch.mock.calls[0][1], templateCtx); + tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); }); afterEach(() => jest.restoreAllMocks()); it('should call createWatch with correct args', () => { - expect(rest.createWatch.mock.calls[0][0]).toBe('apm-mocked-uuid'); + expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid'); }); it('should format slack message correctly', () => { @@ -78,7 +86,7 @@ describe('createErrorGroupWatch', () => { }); it('should return watch id', async () => { - const id = rest.createWatch.mock.calls[0][0]; + const id = createWatchSpy.mock.calls[0][0].id; expect(createWatchResponse).toEqual(id); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index e7d06403b8f8e..1d21e35f122d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; +import { HttpServiceBase } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -17,7 +18,6 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; -// @ts-ignore import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { @@ -35,6 +35,7 @@ export interface Schedule { } interface Arguments { + http: HttpServiceBase; emails: string[]; schedule: Schedule; serviceName: string; @@ -54,6 +55,7 @@ interface Actions { } export async function createErrorGroupWatch({ + http, emails = [], schedule, serviceName, @@ -250,6 +252,10 @@ export async function createErrorGroupWatch({ }; } - await createWatch(id, body); + await createWatch({ + http, + id, + watch: body + }); return id; } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index e1b61d06e3559..887200bdfc22a 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -9,10 +9,11 @@ import LRU from 'lru-cache'; import hash from 'object-hash'; import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; -export type FetchOptions = HttpFetchOptions & { +export type FetchOptions = Omit & { pathname: string; forceCache?: boolean; method?: string; + body?: any; }; function fetchOptionsWithDebug(fetchOptions: FetchOptions) { @@ -26,9 +27,7 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) { const body = isGet ? {} : { - body: JSON.stringify( - fetchOptions.body || ({} as HttpFetchOptions['body']) - ) + body: JSON.stringify(fetchOptions.body || {}) }; return { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts similarity index 60% rename from x-pack/legacy/plugins/apm/public/services/rest/watcher.js rename to x-pack/legacy/plugins/apm/public/services/rest/watcher.ts index 9d68a1665912c..dfa64b5368ee9 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpServiceBase } from 'kibana/public'; import { callApi } from './callApi'; -export async function createWatch(id, watch) { - return callApi({ +export async function createWatch({ + id, + watch, + http +}: { + http: HttpServiceBase; + id: string; + watch: any; +}) { + return callApi(http, { method: 'PUT', pathname: `/api/watcher/watch/${id}`, - body: JSON.stringify({ type: 'json', id, watch }) + body: { type: 'json', id, watch } }); } From c8f0a751a775b2dbe1f8f26629ed00326a4a3479 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 26 Nov 2019 12:20:37 -0500 Subject: [PATCH 52/63] [Timepicker] Ensure we filter out undefined values (#51458) * Fix error with undefined from or to * PR feedback * Remove unnecessary test --- src/plugins/data/public/query/timefilter/time_history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index 4dabbb557e9db..fe73fd85b164d 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -37,7 +37,7 @@ export class TimeHistory { } add(time: TimeRange) { - if (!time) { + if (!time || !time.from || !time.to) { return; } From 84489619bbc1f7d0e0d6104e0248116460074227 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 26 Nov 2019 18:21:02 +0100 Subject: [PATCH 53/63] Upgrade typescript-eslint to 2.9.0 (#51737) * Upgrade typescript-eslint to 2.9.0 * Remove redundant APM eslint disable --- package.json | 4 +- packages/eslint-config-kibana/package.json | 4 +- .../avg_duration_by_browser/transformer.ts | 2 - yarn.lock | 40 +++++++++---------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a4f7b869aef6f..ce43089105268 100644 --- a/package.json +++ b/package.json @@ -349,8 +349,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 71517bc10404d..ee65a1cf79148 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts index 805f8f192bdb1..5d140155f75e4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts @@ -14,8 +14,6 @@ export function transformer({ response: ESResponse; }): AvgDurationByBrowserAPIResponse { const allUserAgentKeys = new Set( - // TODO(TS-3.7-ESLINT) - // eslint-disable-next-line @typescript-eslint/camelcase (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) => key.toString() ) diff --git a/yarn.lock b/yarn.lock index 7e965979fd46f..1cf41a3ecd57c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4154,24 +4154,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz#eca584d46094ebebc3cb3e9fb625bfbc904a534d" - integrity sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA== +"@typescript-eslint/eslint-plugin@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz#fa810282c0e45f6c2310b9c0dfcd25bff97ab7e9" + integrity sha512-98rfOt3NYn5Gr9wekTB8TexxN6oM8ZRvYuphPs1Atfsy419SDLYCaE30aJkRiiTCwGEY98vOhFsEVm7Zs4toQQ== dependencies: - "@typescript-eslint/experimental-utils" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" - integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== +"@typescript-eslint/experimental-utils@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz#bbe99a8d9510240c055fc4b17230dd0192ba3c7f" + integrity sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4183,14 +4183,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.8.0.tgz#e10f7c40c8cf2fb19920c879311e6c46ad17bacb" - integrity sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw== +"@typescript-eslint/parser@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.9.0.tgz#2e9cf438de119b143f642a3a406e1e27eb70b7cd" + integrity sha512-fJ+dNs3CCvEsJK2/Vg5c2ZjuQ860ySOAsodDPwBaVlrGvRN+iCNC8kUfLFL8cT49W4GSiLPa/bHiMjYXA7EhKQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.8.0" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4201,10 +4201,10 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" - integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== +"@typescript-eslint/typescript-estree@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz#09138daf8f47d0e494ba7db9e77394e928803017" + integrity sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From 3b6e51b2d8078180a0e6e7cfdadead4d6ae07ead Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 26 Nov 2019 19:56:52 +0100 Subject: [PATCH 54/63] [SIEM] Fix styled-components bump in siem app (#51559) --- .../components/edit_data_provider/index.tsx | 176 +++++++++--------- .../siem/public/components/loading/index.tsx | 46 ++--- .../siem/public/components/page/index.tsx | 16 +- .../column_headers/events_select/index.tsx | 4 +- .../timeline/data_providers/empty.tsx | 2 +- .../timeline/data_providers/providers.tsx | 2 +- .../components/timeline/properties/index.tsx | 39 ---- .../search_or_filter/search_or_filter.tsx | 88 ++++----- .../public/components/wrapper_page/index.tsx | 2 + 9 files changed, 177 insertions(+), 198 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index 214ac926e8868..18b271a3abc29 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -49,8 +49,7 @@ HeaderContainer.displayName = 'HeaderContainer'; // SIDE EFFECT: the following `createGlobalStyle` overrides the default styling // of euiComboBoxOptionsList because it's implemented as a popover, so it's // not selectable as a child of the styled component -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const StatefulEditDataProviderGlobalStyle = createGlobalStyle` .euiComboBoxOptionsList { z-index: 9999; } @@ -158,104 +157,107 @@ export const StatefulEditDataProvider = React.memo( }, []); return ( - - - - - - - 0 ? updatedField[0].label : null}> + <> + + + + + + + 0 ? updatedField[0].label : null}> + + + + + + + - - - + + + + + + + + + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - + - - - - - - + ) : null} - {updatedOperator.length > 0 && - updatedOperator[0].label !== i18n.EXISTS && - updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - - + - ) : null} - - - - - - - - { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} - size="s" - > - {i18n.SAVE} - - - - - - + + + + { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + }); + }} + size="s" + > + {i18n.SAVE} + + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx index eb85edce78a8f..42867c09b971b 100644 --- a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx @@ -10,8 +10,7 @@ import { pure } from 'recompose'; import styled, { createGlobalStyle } from 'styled-components'; // SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const LoadingPanelGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } @@ -41,27 +40,30 @@ export const LoadingPanel = pure( position = 'relative', zIndex = 'inherit', }) => ( - - - - - - - + <> + + + + + + + - - {text} - - - - - + + {text} + + + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index bc701006c3a9c..d56012de88929 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -15,9 +15,12 @@ import { } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; -// SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +/* + SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly + and `EuiPopover`, `EuiToolTip` global styles +*/ + +export const AppGlobalStyle = createGlobalStyle` div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -25,6 +28,13 @@ createGlobalStyle` div.application { background-color: rgba(0,0,0,0); } + + .euiPopover__panel.euiPopover__panel-isOpen { + z-index: 9900 !important; + } + .euiToolTip { + z-index: 9950 !important; + } `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index 747ef8f3ffe47..4f414af74a914 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -17,8 +17,7 @@ export const EVENTS_SELECT_WIDTH = 60; // px // SIDE EFFECT: the following `createGlobalStyle` overrides // the style of the select items -// eslint-disable-next-line -createGlobalStyle` +const EventsSelectGlobalStyle = createGlobalStyle` .eventsSelectItem { width: 100% !important; @@ -73,6 +72,7 @@ export const EventsSelect = pure(({ checkState, timelineId }) => { /> +
); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 29d2df5172457..3ef7240ee0375 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -50,7 +50,7 @@ const HighlightedBackground = styled.span` HighlightedBackground.displayName = 'HighlightedBackground'; const EmptyContainer = styled.div<{ showSmallMsg: boolean }>` - width: ${props => (props.showSmallMsg ? '60px' : 'auto')} + width: ${props => (props.showSmallMsg ? '60px' : 'auto')}; align-items: center; display: flex; flex-direction: row; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 112962367cd36..5a8654509fa88 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -46,7 +46,7 @@ interface Props { const ROW_OF_DATA_PROVIDERS_HEIGHT = 43; // px const PanelProviders = styled.div` - position: relative + position: relative; display: flex; flex-direction: row; min-height: 100px; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index ccc222673d7bc..7b69e006f48ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import styled, { createGlobalStyle } from 'styled-components'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -22,43 +20,6 @@ type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; -// SIDE EFFECT: the following `createGlobalStyle` overrides `EuiPopover` -// and `EuiToolTip` global styles: -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - } - .euiToolTip { - z-index: 9950 !important; - } -`; - -const Avatar = styled(EuiAvatar)` - margin-left: 5px; -`; - -Avatar.displayName = 'Avatar'; - -const DescriptionPopoverMenuContainer = styled.div` - margin-top: 15px; -`; - -DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; - -const SettingsIcon = styled(EuiIcon)` - margin-left: 4px; - cursor: pointer; -`; - -SettingsIcon.displayName = 'SettingsIcon'; - -const HiddenFlexItem = styled(EuiFlexItem)` - display: none; -`; - -HiddenFlexItem.displayName = 'HiddenFlexItem'; - interface Props { associateNote: AssociateNote; createTimeline: CreateTimeline; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 2d953ce3cfc95..eaa476bf3e2b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -26,8 +26,7 @@ const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; const searchOrFilterPopoverWidth = '352px'; // SIDE EFFECT: the following creates a global class selector -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const SearchOrFilterGlobalStyle = createGlobalStyle` .${timelineSelectModeItemsClassName} { width: 350px !important; } @@ -110,48 +109,51 @@ export const SearchOrFilter = pure( updateKqlMode, updateReduxTime, }) => ( - - - - - updateKqlMode({ id: timelineId, kqlMode: mode })} - options={options} - popoverClassName={searchOrFilterPopoverClassName} - valueOfSelected={kqlMode} + <> + + + + + updateKqlMode({ id: timelineId, kqlMode: mode })} + options={options} + popoverClassName={searchOrFilterPopoverClassName} + valueOfSelected={kqlMode} + /> + + + + - - - - - - - + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx index 5998aa527206e..309693427459e 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; +import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` ${({ theme }) => css` @@ -54,6 +55,7 @@ export const WrapperPage = React.memo( return ( {children} + ); } From f5296293c25504f280ce18c70f90f9e5e53e95c3 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 26 Nov 2019 14:22:02 -0500 Subject: [PATCH 55/63] [Canvas] Move workpad api routes to New Platform (#51116) * Move workpad api routes to New Platform * Cleanup * Clean up/Pr Feedback * Adding missing dependency to tests * Fix typecheck * Loosen workpad schema restrictions --- .../canvas/__tests__/fixtures/workpads.ts | 13 + .../canvas/public/lib/workpad_service.js | 1 + .../plugins/canvas/server/routes/index.ts | 2 - .../canvas/server/routes/workpad.test.js | 462 ------------------ .../plugins/canvas/server/routes/workpad.ts | 254 ---------- x-pack/plugins/canvas/kibana.json | 10 + x-pack/plugins/canvas/server/index.ts | 11 + x-pack/plugins/canvas/server/plugin.ts | 25 + .../server/routes/catch_error_handler.ts | 30 ++ x-pack/plugins/canvas/server/routes/index.ts | 17 + .../server/routes/workpad/create.test.ts | 102 ++++ .../canvas/server/routes/workpad/create.ts | 57 +++ .../server/routes/workpad/delete.test.ts | 78 +++ .../canvas/server/routes/workpad/delete.ts | 32 ++ .../canvas/server/routes/workpad/find.test.ts | 113 +++++ .../canvas/server/routes/workpad/find.ts | 60 +++ .../canvas/server/routes/workpad/get.test.ts | 140 ++++++ .../canvas/server/routes/workpad/get.ts | 65 +++ .../canvas/server/routes/workpad/index.ts | 21 + .../server/routes/workpad/ok_response.ts | 9 + .../server/routes/workpad/update.test.ts | 223 +++++++++ .../canvas/server/routes/workpad/update.ts | 129 +++++ .../server/routes/workpad/workpad_schema.ts | 65 +++ 23 files changed, 1201 insertions(+), 718 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.test.js delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.ts create mode 100644 x-pack/plugins/canvas/kibana.json create mode 100644 x-pack/plugins/canvas/server/index.ts create mode 100644 x-pack/plugins/canvas/server/plugin.ts create mode 100644 x-pack/plugins/canvas/server/routes/catch_error_handler.ts create mode 100644 x-pack/plugins/canvas/server/routes/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/ok_response.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts index d7ebbd87c97e6..271fc7a979057 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -192,3 +192,16 @@ export const elements: CanvasElement[] = [ { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, { ...BaseElement, expression: 'image | render' }, ]; + +export const workpadWithGroupAsElement: CanvasWorkpad = { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'image | render' }, + { ...BaseElement, id: 'group-1234' }, + ], + }, + ], +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index 33067f1837f41..f1ed069c15d4d 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -29,6 +29,7 @@ export function get(workpadId) { }); } +// TODO: I think this function is never used. Look into and remove the corresponding route as well export function update(id, workpad) { return fetch.put(`${apiPath}/${id}`, workpad); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index a0502c5e891a2..515d5b5e895ed 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { workpad } from './workpad'; import { esFields } from './es_fields'; import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; @@ -13,6 +12,5 @@ import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); - workpad(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js b/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js deleted file mode 100644 index 09a5c3b89c31e..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { workpad } from './workpad'; - -const routePrefix = API_ROUTE_WORKPAD; -const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; -const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - -jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); - -describe(`${CANVAS_TYPE} API`, () => { - const savedObjectsClient = { - get: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - }; - - afterEach(() => { - savedObjectsClient.get.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.find.mockReset(); - }); - - // Mock toISOString function of all Date types - global.Date = class Date extends global.Date { - toISOString() { - return '2019-02-12T21:01:22.479Z'; - } - }; - - // Setup mock server - const mockServer = new Hapi.Server({ debug: false, port: 0 }); - const mockEs = { - getCluster: () => ({ - errors: { - // formatResponse will fail without objects here - '400': Error, - '401': Error, - '403': Error, - '404': Error, - }, - }), - }; - - mockServer.ext('onRequest', (req, h) => { - req.getSavedObjectsClient = () => savedObjectsClient; - return h.continue; - }); - workpad(mockServer.route.bind(mockServer), mockEs); - - describe(`GET ${routePrefix}/{id}`, () => { - test('returns successful response', async () => { - const request = { - method: 'GET', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ id: '123', attributes: { foo: true } }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "foo": true, - "id": "123", -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - describe(`POST ${routePrefix}`, () => { - test('returns successful response without id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "workpad-123abc", - }, - ], -] -`); - }); - - test('returns succesful response with id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - id: '123', - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'PUT', - url: `${routePrefix}/123`, - payload: { - id: '234', - foo: true, - }, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`DELETE ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'DELETE', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.delete.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.delete.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - it(`GET ${routePrefix}/find`, async () => { - const request = { - method: 'GET', - url: `${routePrefix}/find?name=abc&page=2&perPage=10`, - }; - - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - attributes: { - foo: true, - }, - }, - ], - }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "workpads": Array [ - Object { - "foo": true, - "id": "1", - }, - ], -} -`); - expect(savedObjectsClient.find.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - Object { - "fields": Array [ - "id", - "name", - "@created", - "@timestamp", - ], - "page": "2", - "perPage": "10", - "search": "abc* | abc", - "searchFields": Array [ - "name", - ], - "sortField": "@timestamp", - "sortOrder": "desc", - "type": "canvas-workpad", - }, - ], -] -`); - }); - - describe(`PUT ${routePrefixAssets}/{id}`, () => { - test('only updates assets', async () => { - const request = { - method: 'PUT', - url: `${routePrefixAssets}/123`, - payload: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - 'asset-456': { - id: 'asset-456', - '@created': '2019-02-15T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }; - - // provide some existing workpad data to check that it's preserved - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - "asset-456": Object { - "@created": "2019-02-15T00:00:00.000Z", - "id": "asset-456", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "name": "fake workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefixStructures}/{id}`, () => { - test('only updates workpad', async () => { - const request = { - method: 'PUT', - url: `${routePrefixStructures}/123`, - payload: { - name: 'renamed workpad', - css: '.canvasPage { color: LavenderBlush; }', - }, - }; - - // provide some existing asset data and a name to replace - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - assets: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "css": ".canvasPage { color: LavenderBlush; }", - "name": "renamed workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts b/x-pack/legacy/plugins/canvas/server/routes/workpad.ts deleted file mode 100644 index 380fe97ca9ef1..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -import { CoreSetup } from '../shim'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CanvasWorkpad } from '../../types'; - -type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface WorkpadRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type WorkpadRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad; -}; - -type FindWorkpadRequest = WorkpadRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -type AssetsRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad['assets']; -}; - -export function workpad( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore EsErrors is not on the Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - const routePrefix = API_ROUTE_WORKPAD; - const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; - const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - const formatResponse = formatRes(esErrors); - - function createWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A workpad payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('workpad') } - ); - } - - function updateWorkpad( - req: WorkpadRequest | AssetsRequest, - newPayload?: CanvasWorkpad | { assets: CanvasWorkpad['assets'] } - ) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpadObject => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...(workpadObject.attributes as SavedObjectAttributes), - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - } - - function deleteWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CANVAS_TYPE, id); - } - - function findWorkpad(req: FindWorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CANVAS_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', '@created', '@timestamp'], - page, - perPage, - }); - } - - // get workpad - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient - .get(CANVAS_TYPE, id) - .then(obj => { - if ( - // not sure if we need to be this defensive - obj.type === 'canvas-workpad' && - obj.attributes && - obj.attributes.pages && - obj.attributes.pages.length - ) { - obj.attributes.pages.forEach(page => { - const elements = (page.elements || []).filter( - ({ id: pageId }) => !pageId.startsWith('group') - ); - const groups = (page.groups || []).concat( - (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) - ); - page.elements = elements; - page.groups = groups; - }); - } - return obj; - }) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse); - }, - }); - - // create workpad - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return createWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad assets - route({ - method: 'PUT', - path: `${routePrefixAssets}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: AssetsRequest) { - const payload = { assets: request.payload }; - return updateWorkpad(request, payload) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad structures - route({ - method: 'PUT', - path: `${routePrefixStructures}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // delete workpad - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler(request: WorkpadRequest) { - return deleteWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // find workpads - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler(request: FindWorkpadRequest) { - return findWorkpad(request) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - workpads: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - workpads: [], - }; - }); - }, - }); -} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json new file mode 100644 index 0000000000000..87214f0287054 --- /dev/null +++ b/x-pack/plugins/canvas/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "canvas", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "canvas"], + "server": true, + "ui": false, + "requiredPlugins": [] + } + \ No newline at end of file diff --git a/x-pack/plugins/canvas/server/index.ts b/x-pack/plugins/canvas/server/index.ts new file mode 100644 index 0000000000000..e881f7db69c78 --- /dev/null +++ b/x-pack/plugins/canvas/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/server'; +import { CanvasPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CanvasPlugin(initializerContext); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts new file mode 100644 index 0000000000000..76b86c2ac39b4 --- /dev/null +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server'; +import { initRoutes } from './routes'; + +export class CanvasPlugin implements Plugin { + private readonly logger: Logger; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(coreSetup: CoreSetup): void { + const canvasRouter = coreSetup.http.createRouter(); + + initRoutes({ router: canvasRouter, logger: this.logger }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts new file mode 100644 index 0000000000000..fb7f4d6ee2600 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; + +export const catchErrorHandler: < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>( + fn: RequestHandler +) => RequestHandler = fn => { + return async (context, request, response) => { + try { + return await fn(context, request, response); + } catch (error) { + if (error.isBoom) { + return response.customError({ + body: error.output.payload, + statusCode: error.output.statusCode, + }); + } + return response.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts new file mode 100644 index 0000000000000..46873a6b32542 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, Logger } from 'src/core/server'; +import { initWorkpadRoutes } from './workpad'; + +export interface RouteInitializerDeps { + router: IRouter; + logger: Logger; +} + +export function initRoutes(deps: RouteInitializerDeps) { + initWorkpadRoutes(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts new file mode 100644 index 0000000000000..dbad1a97dc458 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateWorkpadRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the workpad is created`, async () => { + const mockWorkpad = { + pages: [], + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: mockWorkpad, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...mockWorkpad, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `workpad-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts new file mode 100644 index 0000000000000..be904356720b6 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD}`, + validate: { + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + if (!request.body) { + return response.badRequest({ body: 'A workpad payload is required' }); + } + + const workpad = request.body as CanvasWorkpad; + + const now = new Date().toISOString(); + const { id, ...payload } = workpad; + + await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('workpad') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts new file mode 100644 index 0000000000000..e693840826b7a --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteWorkpadRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the workpad is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith(CANVAS_TYPE, id); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts new file mode 100644 index 0000000000000..7adf11e7a887b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + context.core.savedObjects.client.delete(CANVAS_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts new file mode 100644 index 0000000000000..08de9b20e9818 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { initializeFindWorkpadsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindWorkpadsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found workpads`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 2, + "workpads": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 0, + "workpads": Array [], + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.ts b/x-pack/plugins/canvas/server/routes/workpad/find.ts new file mode 100644 index 0000000000000..a528a75611609 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindWorkpadsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const workpads = await savedObjectsClient.find({ + type: CANVAS_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: ['id', 'name', '@created', '@timestamp'], + page, + perPage, + }); + + return response.ok({ + body: { + total: workpads.total, + workpads: workpads.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + workpads: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts new file mode 100644 index 0000000000000..a31293f572c75 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetWorkpadRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpadWithGroupAsElement } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the workpad is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-workpad", + "123", + ], + ] + `); + }); + + it('corrects elements that should be groups', async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: workpadWithGroupAsElement as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + const workpad = response.payload as CanvasWorkpad; + + expect(response.status).toBe(200); + expect(workpad).not.toBeUndefined(); + + expect(workpad.pages[0].elements.length).toBe(1); + expect(workpad.pages[0].groups.length).toBe(1); + }); + + it('returns 404 if the workpad is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-workpad/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts new file mode 100644 index 0000000000000..7a51006aa9f02 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + request.params.id + ); + + if ( + // not sure if we need to be this defensive + workpad.type === 'canvas-workpad' && + workpad.attributes && + workpad.attributes.pages && + workpad.attributes.pages.length + ) { + workpad.attributes.pages.forEach(page => { + const elements = (page.elements || []).filter( + ({ id: pageId }) => !pageId.startsWith('group') + ); + const groups = (page.groups || []).concat( + (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) + ); + page.elements = elements; + page.groups = groups; + }); + } + + return response.ok({ + body: { + id: workpad.id, + ...workpad.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts new file mode 100644 index 0000000000000..8a61b30be5414 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeFindWorkpadsRoute } from './find'; +import { initializeGetWorkpadRoute } from './get'; +import { initializeCreateWorkpadRoute } from './create'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { initializeDeleteWorkpadRoute } from './delete'; + +export function initWorkpadRoutes(deps: RouteInitializerDeps) { + initializeFindWorkpadsRoute(deps); + initializeGetWorkpadRoute(deps); + initializeCreateWorkpadRoute(deps); + initializeUpdateWorkpadRoute(deps); + initializeUpdateWorkpadAssetsRoute(deps); + initializeDeleteWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts new file mode 100644 index 0000000000000..43d545a5183fe --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const okResponse = { + ok: true, +}; diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts new file mode 100644 index 0000000000000..492a6c98d71ee --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { okResponse } from './ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const workpad = workpads[0]; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('PUT workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the workpad is updated`, async () => { + const updatedWorkpad = { name: 'new name' }; + const { id, ...workpadAttributes } = workpad; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + body: updatedWorkpad, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: workpadAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...workpadAttributes, + ...updatedWorkpad, + '@timestamp': nowIso, + '@created': workpad['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing workpad is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CANVAS_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); + +describe('update assets', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadAssetsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it('updates assets', async () => { + const { id, ...attributes } = workpad; + const assets = { + 'asset-1': { + '@created': new Date().toISOString(), + id: 'asset-1', + type: 'asset', + value: 'some-url-encoded-asset', + }, + 'asset-2': { + '@created': new Date().toISOString(), + id: 'asset-2', + type: 'asset', + value: 'some-other asset', + }, + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad-assets/some-id', + params: { + id, + }, + body: assets, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: attributes as any, + references: [], + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...attributes, + '@timestamp': nowIso, + assets, + }, + { + id, + overwrite: true, + } + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts new file mode 100644 index 0000000000000..460aa174038ae --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { KibanaResponseFactory } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_STRUCTURES, + API_ROUTE_WORKPAD_ASSETS, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); + +const AssetPayloadSchema = schema.object({ + assets: AssetsRecordSchema, +}); + +const workpadUpdateHandler = async ( + payload: TypeOf | TypeOf, + id: string, + savedObjectsClient: SavedObjectsClientContract, + response: KibanaResponseFactory +) => { + const now = new Date().toISOString(); + + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( + CANVAS_TYPE, + { + ...workpadObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); +}; + +export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + // TODO: This route is likely deprecated and everything is using the workpad_structures + // path instead. Investigate further. + router.put( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); + + router.put( + { + path: `${API_ROUTE_WORKPAD_STRUCTURES}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); +} + +export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + + router.put( + { + path: `${API_ROUTE_WORKPAD_ASSETS}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + // ToDo: Currently the validation must be a schema.object + // Because we don't know what keys the assets will have, we have to allow + // unknowns and then validate in the handler + body: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, response) => { + return workpadUpdateHandler( + { assets: AssetsRecordSchema.validate(request.body) }, + request.params.id, + context.core.savedObjects.client, + response + ); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts new file mode 100644 index 0000000000000..0bcb161575901 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const WorkpadElementSchema = schema.object({ + expression: schema.string(), + filter: schema.maybe(schema.nullable(schema.string())), + id: schema.string(), + position: PositionSchema, +}); + +export const WorkpadPageSchema = schema.object({ + elements: schema.arrayOf(WorkpadElementSchema), + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }) + ), + id: schema.string(), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const WorkpadAssetSchema = schema.object({ + '@created': schema.string(), + id: schema.string(), + type: schema.string(), + value: schema.string(), +}); + +export const WorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}); From c41531122183749159a06641acef22ad5a230019 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 26 Nov 2019 23:28:04 +0300 Subject: [PATCH 56/63] Move @kbn/es-query into data plugin (#51014) --- .eslintignore | 2 +- .i18nrc.json | 1 - ...a-plugin-public.savedobjectsclient.find.md | 2 +- ...kibana-plugin-public.savedobjectsclient.md | 2 +- package.json | 1 - packages/kbn-es-query/README.md | 92 ---- packages/kbn-es-query/babel.config.js | 35 -- packages/kbn-es-query/index.d.ts | 20 - packages/kbn-es-query/package.json | 28 -- packages/kbn-es-query/scripts/build.js | 20 - .../src/__fixtures__/filter_skeleton.json | 5 - .../__fixtures__/index_pattern_response.json | 303 ------------- packages/kbn-es-query/src/index.d.ts | 20 - packages/kbn-es-query/src/index.js | 20 - .../src/kuery/ast/__tests__/ast.js | 415 ----------------- packages/kbn-es-query/src/kuery/ast/ast.d.ts | 50 --- .../kbn-es-query/src/kuery/ast/index.d.ts | 20 - .../functions/__tests__/geo_bounding_box.js | 120 ----- .../kuery/functions/__tests__/geo_polygon.js | 131 ------ .../src/kuery/functions/__tests__/is.js | 310 ------------- .../utils/get_full_field_name_node.js | 88 ---- .../kuery/node_types/__tests__/function.js | 80 ---- .../kuery/node_types/__tests__/named_arg.js | 62 --- .../kuery/node_types/__tests__/wildcard.js | 107 ----- .../__tests__/get_time_zone_from_settings.js | 36 -- packages/kbn-es-query/src/utils/filters.js | 133 ------ .../src/utils/get_time_zone_from_settings.js | 28 -- packages/kbn-es-query/src/utils/index.js | 20 - packages/kbn-es-query/tasks/build_cli.js | 103 ----- packages/kbn-es-query/tsconfig.browser.json | 11 - packages/kbn-es-query/tsconfig.json | 11 - src/core/public/public.api.md | 2 +- .../service/lib/filter_utils.test.ts | 22 +- .../saved_objects/service/lib/filter_utils.ts | 76 ++-- .../service/lib/repository.test.js | 3 +- .../saved_objects/service/lib/repository.ts | 8 +- .../service/lib/search_dsl/query_params.ts | 6 +- .../service/lib/search_dsl/search_dsl.ts | 4 +- .../components/query_bar_top_row.tsx | 7 +- .../public/vis/timelion_request_handler.ts | 2 +- .../public/components/vis_editor.js | 4 +- .../public/vega_request_handler.ts | 2 +- .../es_query/es_query/build_es_query.test.ts | 2 +- .../es_query/es_query/build_es_query.ts | 2 +- .../es_query/filter_matches_index.test.ts | 3 +- .../es_query/es_query/filter_matches_index.ts | 2 +- .../common/es_query/es_query/from_filters.ts | 2 +- .../es_query/es_query/from_kuery.test.ts | 4 +- .../common/es_query/es_query/from_kuery.ts | 30 +- .../es_query/es_query/migrate_filter.test.ts | 6 +- .../es_query/es_query/migrate_filter.ts | 2 +- src/plugins/data/common/es_query/index.ts | 3 +- .../es_query/kuery/ast/_generated_}/kuery.js | 0 .../common/es_query/kuery/ast/ast.test.ts | 421 ++++++++++++++++++ .../data/common/es_query/kuery/ast/ast.ts | 89 ++-- .../data/common/es_query/kuery/ast/index.ts | 0 .../data/common/es_query}/kuery/ast/kuery.peg | 0 .../common/es_query}/kuery/functions/and.js | 0 .../es_query/kuery/functions/and.test.ts | 54 ++- .../es_query}/kuery/functions/exists.js | 0 .../es_query/kuery/functions/exists.test.ts | 73 +-- .../kuery/functions/geo_bounding_box.js | 0 .../kuery/functions/geo_bounding_box.test.ts | 133 ++++++ .../es_query}/kuery/functions/geo_polygon.js | 0 .../kuery/functions/geo_polygon.test.ts | 143 ++++++ .../common/es_query}/kuery/functions/index.js | 0 .../common/es_query}/kuery/functions/is.js | 20 +- .../es_query/kuery/functions/is.test.ts | 305 +++++++++++++ .../es_query}/kuery/functions/nested.js | 0 .../es_query/kuery/functions/nested.test.ts | 56 ++- .../common/es_query}/kuery/functions/not.js | 0 .../es_query/kuery/functions/not.test.ts | 52 ++- .../common/es_query}/kuery/functions/or.js | 0 .../es_query/kuery/functions/or.test.ts | 63 +-- .../common/es_query}/kuery/functions/range.js | 2 +- .../es_query/kuery/functions/range.test.ts | 206 +++++---- .../kuery/functions/utils/get_fields.js | 0 .../kuery/functions/utils/get_fields.test.ts | 73 ++- .../utils/get_full_field_name_node.js | 0 .../utils/get_full_field_name_node.test.ts | 87 ++++ .../data/common/es_query/kuery/index.ts | 6 +- .../es_query/kuery/kuery_syntax_error.test.ts | 71 +-- .../es_query/kuery/kuery_syntax_error.ts | 57 ++- .../es_query}/kuery/node_types/function.js | 0 .../kuery/node_types/function.test.ts | 75 ++++ .../es_query}/kuery/node_types/index.d.ts | 44 +- .../es_query}/kuery/node_types/index.js | 0 .../es_query}/kuery/node_types/literal.js | 0 .../es_query/kuery/node_types/literal.test.ts | 35 +- .../es_query}/kuery/node_types/named_arg.js | 0 .../kuery/node_types/named_arg.test.ts | 57 +++ .../es_query}/kuery/node_types/wildcard.js | 0 .../kuery/node_types/wildcard.test.ts | 110 +++++ .../data/common/es_query/kuery/types.ts | 23 +- .../common/field_formats/converters/custom.ts | 4 +- .../data/common/field_formats/field_format.ts | 2 +- tasks/config/peg.js | 4 +- .../components/shared/KueryBar/index.tsx | 12 +- .../convert_ui_filters/get_ui_filters_es.ts | 14 +- .../public/lib/adapters/elasticsearch/rest.ts | 9 +- .../graph/public/components/search_bar.tsx | 9 +- .../components/metrics_explorer/kuery_bar.tsx | 4 +- .../store/local/log_filter/selectors.ts | 6 +- .../store/local/waffle_filter/selectors.ts | 5 +- .../plugins/infra/public/utils/kuery.ts | 6 +- .../public/autocomplete_providers/index.js | 4 +- .../components/kql_filter_bar/utils.js | 8 +- .../plugins/siem/public/lib/keury/index.ts | 12 +- .../components/step_define_rule/schema.tsx | 4 +- .../transform/public/app/lib/kibana/common.ts | 2 +- .../components/functional/kuery_bar/index.tsx | 17 +- .../plugins/uptime/public/pages/overview.tsx | 7 +- x-pack/package.json | 1 - .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 12 +- 115 files changed, 2025 insertions(+), 2852 deletions(-) delete mode 100644 packages/kbn-es-query/README.md delete mode 100644 packages/kbn-es-query/babel.config.js delete mode 100644 packages/kbn-es-query/index.d.ts delete mode 100644 packages/kbn-es-query/package.json delete mode 100644 packages/kbn-es-query/scripts/build.js delete mode 100644 packages/kbn-es-query/src/__fixtures__/filter_skeleton.json delete mode 100644 packages/kbn-es-query/src/__fixtures__/index_pattern_response.json delete mode 100644 packages/kbn-es-query/src/index.d.ts delete mode 100644 packages/kbn-es-query/src/index.js delete mode 100644 packages/kbn-es-query/src/kuery/ast/__tests__/ast.js delete mode 100644 packages/kbn-es-query/src/kuery/ast/ast.d.ts delete mode 100644 packages/kbn-es-query/src/kuery/ast/index.d.ts delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/is.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/function.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js delete mode 100644 packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js delete mode 100644 packages/kbn-es-query/src/utils/filters.js delete mode 100644 packages/kbn-es-query/src/utils/get_time_zone_from_settings.js delete mode 100644 packages/kbn-es-query/src/utils/index.js delete mode 100644 packages/kbn-es-query/tasks/build_cli.js delete mode 100644 packages/kbn-es-query/tsconfig.browser.json delete mode 100644 packages/kbn-es-query/tsconfig.json rename {packages/kbn-es-query/src/kuery/ast => src/plugins/data/common/es_query/kuery/ast/_generated_}/kuery.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/ast/ast.test.ts rename packages/kbn-es-query/src/kuery/ast/ast.js => src/plugins/data/common/es_query/kuery/ast/ast.ts (53%) rename packages/kbn-es-query/src/kuery/ast/index.js => src/plugins/data/common/es_query/kuery/ast/index.ts (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/ast/kuery.peg (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/and.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/and.js => src/plugins/data/common/es_query/kuery/functions/and.test.ts (50%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/exists.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/exists.js => src/plugins/data/common/es_query/kuery/functions/exists.test.ts (51%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/geo_bounding_box.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/geo_polygon.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/index.js (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/is.js (95%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/is.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/nested.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/nested.js => src/plugins/data/common/es_query/kuery/functions/nested.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/not.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/not.js => src/plugins/data/common/es_query/kuery/functions/not.test.ts (50%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/or.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/or.js => src/plugins/data/common/es_query/kuery/functions/or.test.ts (52%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/range.js (98%) rename packages/kbn-es-query/src/kuery/functions/__tests__/range.js => src/plugins/data/common/es_query/kuery/functions/range.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/utils/get_fields.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js => src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts (52%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/utils/get_full_field_name_node.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts rename packages/kbn-es-query/src/kuery/index.js => src/plugins/data/common/es_query/kuery/index.ts (91%) rename packages/kbn-es-query/src/kuery/errors/index.test.js => src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts (66%) rename packages/kbn-es-query/src/kuery/errors/index.js => src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts (55%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/function.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/function.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/index.d.ts (72%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/index.js (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/literal.js (100%) rename packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js => src/plugins/data/common/es_query/kuery/node_types/literal.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/named_arg.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/wildcard.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts rename packages/kbn-es-query/src/kuery/index.d.ts => src/plugins/data/common/es_query/kuery/types.ts (73%) diff --git a/.eslintignore b/.eslintignore index cf13fc28467d9..90155ca9cb681 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,6 +8,7 @@ bower_components /plugins /built_assets /html_docs +/src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/fixtures/vislib/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts @@ -19,7 +20,6 @@ bower_components /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana -/packages/kbn-es-query/src/kuery/ast/kuery.js /packages/kbn-pm/dist /packages/kbn-plugin-generator/sao_template/template /packages/kbn-ui-framework/dist diff --git a/.i18nrc.json b/.i18nrc.json index 2cdf7d2b039c6..e5ba6762da154 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -16,7 +16,6 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "kbnESQuery": "packages/kbn-es-query", "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 866755e78648a..cecceb04240e6 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 50451b813a61c..c4ceb47f66e1b 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/package.json b/package.json index ce43089105268..2c8d4ad4307b1 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "@kbn/babel-code-parser": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", diff --git a/packages/kbn-es-query/README.md b/packages/kbn-es-query/README.md deleted file mode 100644 index fc403447877d8..0000000000000 --- a/packages/kbn-es-query/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# kbn-es-query - -This module is responsible for generating Elasticsearch queries for Kibana. See explanations below for each of the subdirectories. - -## es_query - -This folder contains the code that combines Lucene/KQL queries and filters into an Elasticsearch query. - -```javascript -buildEsQuery(indexPattern, queries, filters, config) -``` - -Generates the Elasticsearch query DSL from combining the queries and filters provided. - -```javascript -buildQueryFromFilters(filters, indexPattern) -``` - -Generates the Elasticsearch query DSL from the given filters. - -```javascript -luceneStringToDsl(query) -``` - -Generates the Elasticsearch query DSL from the given Lucene query. - -```javascript -migrateFilter(filter, indexPattern) -``` - -Migrates a filter from a previous version of Elasticsearch to the current version. - -```javascript -decorateQuery(query, queryStringOptions) -``` - -Decorates an Elasticsearch query_string query with the given options. - -## filters - -This folder contains the code related to Kibana Filter objects, including their definitions, and helper functions to create them. Filters in Kibana always contain a `meta` property which describes which `index` the filter corresponds to, as well as additional data about the specific filter. - -The object that is created by each of the following functions corresponds to a Filter object in the `lib` directory (e.g. `PhraseFilter`, `RangeFilter`, etc.) - -```javascript -buildExistsFilter(field, indexPattern) -``` - -Creates a filter (`ExistsFilter`) where the given field exists. - -```javascript -buildPhraseFilter(field, value, indexPattern) -``` - -Creates an filter (`PhraseFilter`) where the given field matches the given value. - -```javascript -buildPhrasesFilter(field, params, indexPattern) -``` - -Creates a filter (`PhrasesFilter`) where the given field matches one or more of the given values. `params` should be an array of values. - -```javascript -buildQueryFilter(query, index) -``` - -Creates a filter (`CustomFilter`) corresponding to a raw Elasticsearch query DSL object. - -```javascript -buildRangeFilter(field, params, indexPattern) -``` - -Creates a filter (`RangeFilter`) where the value for the given field is in the given range. `params` should contain `lt`, `lte`, `gt`, and/or `gte`. - -## kuery - -This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language. - -In general, you will only need to worry about the following functions from the `ast` folder: - -```javascript -fromExpression(expression) -``` - -Generates an abstract syntax tree corresponding to the raw Kibana query `expression`. - -```javascript -toElasticsearchQuery(node, indexPattern) -``` - -Takes an abstract syntax tree (generated from the previous method) and generates the Elasticsearch query DSL using the given `indexPattern`. Note that if no `indexPattern` is provided, then an Elasticsearch query DSL will still be generated, ignoring things like the index pattern scripted fields, field types, etc. - diff --git a/packages/kbn-es-query/babel.config.js b/packages/kbn-es-query/babel.config.js deleted file mode 100644 index 68783433fc711..0000000000000 --- a/packages/kbn-es-query/babel.config.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// We can't use common Kibana presets here because of babel versions incompatibility -module.exports = { - env: { - public: { - presets: [ - '@kbn/babel-preset/webpack_preset' - ], - }, - server: { - presets: [ - '@kbn/babel-preset/node_preset' - ], - }, - }, - ignore: ['**/__tests__/**/*', '**/*.test.ts', '**/*.test.tsx'], -}; diff --git a/packages/kbn-es-query/index.d.ts b/packages/kbn-es-query/index.d.ts deleted file mode 100644 index 9bbd0a193dfed..0000000000000 --- a/packages/kbn-es-query/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './src'; diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json deleted file mode 100644 index 2cd2a8f53d2ee..0000000000000 --- a/packages/kbn-es-query/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@kbn/es-query", - "main": "target/server/index.js", - "browser": "target/public/index.js", - "version": "1.0.0", - "license": "Apache-2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --source-maps --watch" - }, - "dependencies": { - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", - "moment-timezone": "^0.5.27", - "@kbn/i18n": "1.0.0" - }, - "devDependencies": { - "@babel/cli": "^7.5.5", - "@babel/core": "^7.5.5", - "@kbn/babel-preset": "1.0.0", - "@kbn/dev-utils": "1.0.0", - "@kbn/expect": "1.0.0", - "del": "^5.1.0", - "getopts": "^2.2.4", - "supports-color": "^7.0.0" - } -} diff --git a/packages/kbn-es-query/scripts/build.js b/packages/kbn-es-query/scripts/build.js deleted file mode 100644 index 6d53a8469b0e0..0000000000000 --- a/packages/kbn-es-query/scripts/build.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -require('../tasks/build_cli'); diff --git a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json b/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json deleted file mode 100644 index 1799d04a0fbd8..0000000000000 --- a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "meta": { - "index": "logstash-*" - } -} diff --git a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json b/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json deleted file mode 100644 index 588e6ada69cfe..0000000000000 --- a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "id": "logstash-*", - "title": "logstash-*", - "fields": [ - { - "name": "bytes", - "type": "number", - "esTypes": ["long"], - "count": 10, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ssl", - "type": "boolean", - "esTypes": ["boolean"], - "count": 20, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@timestamp", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "time", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@tags", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "utc_time", - "type": "date", - "esTypes": ["date"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "phpmemory", - "type": "number", - "esTypes": ["integer"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ip", - "type": "ip", - "esTypes": ["ip"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "request_body", - "type": "attachment", - "esTypes": ["attachment"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "point", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "area", - "type": "geo_shape", - "esTypes": ["geo_shape"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "hashed", - "type": "murmur3", - "esTypes": ["murmur3"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "geo.coordinates", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "extension", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "machine.os", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "machine.os.raw", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true, - "subType": { "multi": { "parent": "machine.os" } } - }, - { - "name": "geo.src", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "_id", - "type": "string", - "esTypes": ["_id"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_type", - "type": "string", - "esTypes": ["_type"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_source", - "type": "_source", - "esTypes": ["_source"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-filterable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-sortable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "custom_user_field", - "type": "conflict", - "esTypes": ["long", "text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "script string", - "type": "string", - "count": 0, - "scripted": true, - "script": "'i am a string'", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script number", - "type": "number", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script date", - "type": "date", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "painless", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script murmur3", - "type": "murmur3", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "nestedField.child", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField" } } - }, - { - "name": "nestedField.nestedChild.doublyNestedChild", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField.nestedChild" } } - } - ] -} diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts deleted file mode 100644 index 79e6903b18644..0000000000000 --- a/packages/kbn-es-query/src/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './kuery'; diff --git a/packages/kbn-es-query/src/index.js b/packages/kbn-es-query/src/index.js deleted file mode 100644 index 79e6903b18644..0000000000000 --- a/packages/kbn-es-query/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './kuery'; diff --git a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js deleted file mode 100644 index 3cbe1203bc533..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as ast from '../ast'; -import expect from '@kbn/expect'; -import { nodeTypes } from '../../node_types/index'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery AST API', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('fromKueryExpression', function () { - - it('should return a match all "is" function for whitespace', function () { - const expected = nodeTypes.function.buildNode('is', '*', '*'); - const actual = ast.fromKueryExpression(' '); - expect(actual).to.eql(expected); - }); - - it('should return an "is" function with a null field for single literals', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression('foo'); - expect(actual).to.eql(expected); - }); - - it('should ignore extraneous whitespace at the beginning and end of the query', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression(' foo '); - expect(actual).to.eql(expected); - }); - - it('should not split on whitespace', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); - const actual = ast.fromKueryExpression('foo bar'); - expect(actual).to.eql(expected); - }); - - it('should support "and" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo and bar'); - expect(actual).to.eql(expected); - }); - - it('should support "or" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo or bar'); - expect(actual).to.eql(expected); - }); - - it('should support negation of queries with a "not" prefix', function () { - const expected = nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]) - ); - const actual = ast.fromKueryExpression('not (foo or bar)'); - expect(actual).to.eql(expected); - }); - - it('"and" should have a higher precedence than "or"', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'bar'), - nodeTypes.function.buildNode('is', null, 'baz'), - ]), - nodeTypes.function.buildNode('is', null, 'qux'), - ]) - ]); - const actual = ast.fromKueryExpression('foo or bar and baz or qux'); - expect(actual).to.eql(expected); - }); - - it('should support grouping to override default precedence', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]), - nodeTypes.function.buildNode('is', null, 'baz'), - ]); - const actual = ast.fromKueryExpression('(foo or bar) and baz'); - expect(actual).to.eql(expected); - }); - - it('should support matching against specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); - const actual = ast.fromKueryExpression('foo:bar'); - expect(actual).to.eql(expected); - }); - - it('should also not split on whitespace when matching specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); - const actual = ast.fromKueryExpression('foo:bar baz'); - expect(actual).to.eql(expected); - }); - - it('should treat quoted values as phrases', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); - const actual = ast.fromKueryExpression('foo:"bar baz"'); - expect(actual).to.eql(expected); - }); - - it('should support a shorthand for matching multiple values against a single field', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]); - const actual = ast.fromKueryExpression('foo:(bar or baz)'); - expect(actual).to.eql(expected); - }); - - it('should support "and" and "not" operators and grouping in the shorthand as well', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]), - nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('is', 'foo', 'qux') - ), - ]); - const actual = ast.fromKueryExpression('foo:((bar or baz) and not qux)'); - expect(actual).to.eql(expected); - }); - - it('should support exclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gt: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lt: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes > 1000 and bytes < 8000'); - expect(actual).to.eql(expected); - }); - - it('should support inclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gte: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lte: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes >= 1000 and bytes <= 8000'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in field names', function () { - const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); - const actual = ast.fromKueryExpression('machine*:osx'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in values', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); - const actual = ast.fromKueryExpression('foo:ba*'); - expect(actual).to.eql(expected); - }); - - it('should create an exists "is" query when a field is given and "*" is the value', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', '*'); - const actual = ast.fromKueryExpression('foo:*'); - expect(actual).to.eql(expected); - }); - - it('should support nested queries indicated by curly braces', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ); - const actual = ast.fromKueryExpression('nestedField:{ childOfNested: foo }'); - expect(actual).to.eql(expected); - }); - - it('should support nested subqueries and subqueries inside nested queries', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), - nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), - ]) - )]); - const actual = ast.fromKueryExpression('response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }'); - expect(actual).to.eql(expected); - }); - - it('should support nested sub-queries inside paren groups', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'bar') - ), - ]) - ]); - const actual = ast.fromKueryExpression('response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )'); - expect(actual).to.eql(expected); - }); - - it('should support nested groups inside other nested groups', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode( - 'nested', - 'nestedChild', - nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') - ) - ); - const actual = ast.fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); - expect(actual).to.eql(expected); - }); - }); - - describe('fromLiteralExpression', function () { - - it('should create literal nodes for unquoted values with correct primitive types', function () { - const stringLiteral = nodeTypes.literal.buildNode('foo'); - const booleanFalseLiteral = nodeTypes.literal.buildNode(false); - const booleanTrueLiteral = nodeTypes.literal.buildNode(true); - const numberLiteral = nodeTypes.literal.buildNode(42); - - expect(ast.fromLiteralExpression('foo')).to.eql(stringLiteral); - expect(ast.fromLiteralExpression('true')).to.eql(booleanTrueLiteral); - expect(ast.fromLiteralExpression('false')).to.eql(booleanFalseLiteral); - expect(ast.fromLiteralExpression('42')).to.eql(numberLiteral); - }); - - it('should allow escaping of special characters with a backslash', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - // yo dawg - const actual = ast.fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); - expect(actual).to.eql(expected); - }); - - it('should support double quoted strings that do not need escapes except for quotes', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - const actual = ast.fromLiteralExpression('"\\():<>\\"*"'); - expect(actual).to.eql(expected); - }); - - it('should support escaped backslashes inside quoted strings', function () { - const expected = nodeTypes.literal.buildNode('\\'); - const actual = ast.fromLiteralExpression('"\\\\"'); - expect(actual).to.eql(expected); - }); - - it('should detect wildcards and build wildcard AST nodes', function () { - const expected = nodeTypes.wildcard.buildNode('foo*bar'); - const actual = ast.fromLiteralExpression('foo*bar'); - expect(actual).to.eql(expected); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given node type\'s ES query representation', function () { - const node = nodeTypes.function.buildNode('exists', 'response'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); - const result = ast.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an empty "and" function for undefined nodes and unknown node types', function () { - const expected = nodeTypes.function.toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); - - expect(ast.toElasticsearchQuery()).to.eql(expected); - - const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - delete noTypeNode.type; - expect(ast.toElasticsearchQuery(noTypeNode)).to.eql(expected); - - const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - unknownTypeNode.type = 'notValid'; - expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected); - }); - - it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); - const result = ast.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - }); - - describe('doesKueryExpressionHaveLuceneSyntaxError', function () { - it('should return true for Lucene ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); - expect(result).to.eql(true); - }); - - it('should return false for KQL ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); - expect(result).to.eql(true); - }); - - it('should return false for KQL exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar:*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); - expect(result).to.eql(true); - }); - - it('should return false for KQL wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene regex', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene fuzziness', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene proximity', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene boosting', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene + operator', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene - operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene && operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene || operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for mixed KQL/Lucene queries', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); - expect(result).to.eql(true); - }); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts deleted file mode 100644 index ef3d0ee828874..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { JsonObject } from '..'; - -/** - * WARNING: these typings are incomplete - */ - -export type KueryNode = any; - -export type DslQuery = any; - -export interface KueryParseOptions { - helpers: { - [key: string]: any; - }; - startRule: string; - allowLeadingWildcards: boolean; -} - -export function fromKueryExpression( - expression: string | DslQuery, - parseOptions?: Partial -): KueryNode; - -export function toElasticsearchQuery( - node: KueryNode, - indexPattern?: any, - config?: Record, - context?: Record -): JsonObject; - -export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean; diff --git a/packages/kbn-es-query/src/kuery/ast/index.d.ts b/packages/kbn-es-query/src/kuery/ast/index.d.ts deleted file mode 100644 index 9e68d01d046cc..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from '../ast/ast'; diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js deleted file mode 100644 index 7afa0fcce1bfe..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as geoBoundingBox from '../geo_bounding_box'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; -const params = { - bottomRight: { - lat: 50.73, - lon: -135.35 - }, - topLeft: { - lat: 73.12, - lon: -174.37 - } -}; - -describe('kuery functions', function () { - describe('geoBoundingBox', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided params as named arguments with "lat, lon" string values', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ , ...args ] } = result; - - args.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['bottomRight', 'topLeft'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - - const expectedParam = params[param.name]; - const expectedLatLon = `${expectedParam.lat}, ${expectedParam.lon}`; - expect(param.value.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_bounding_box query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should return an ES geo_bounding_box query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result.geo_bounding_box.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); - expect(geoBoundingBox.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo bounding box query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box).to.have.property('nestedField.geo'); - }); - - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js deleted file mode 100644 index c1f2fae0bb3e1..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as geoPolygon from '../geo_polygon'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - - -let indexPattern; -const points = [ - { - lat: 69.77, - lon: -171.56 - }, - { - lat: 50.06, - lon: -169.10 - }, - { - lat: 69.16, - lon: -125.85 - } -]; - -describe('kuery functions', function () { - - describe('geoPolygon', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoPolygon.buildNodeParams('geo', points); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided points literal "lat, lon" string values', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ , ...args ] } = result; - - args.forEach((param, index) => { - expect(param).to.have.property('type', 'literal'); - const expectedPoint = points[index]; - const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; - expect(param.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_polygon query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should return an ES geo_polygon query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result.geo_polygon.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); - expect(geoPolygon.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo polygon query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon).to.have.property('nestedField.geo'); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js deleted file mode 100644 index b2f3d7ec16a65..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as is from '../is'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery functions', function () { - - describe('is', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('fieldName and value should be required arguments', function () { - expect(is.buildNodeParams).to.throwException(/fieldName is a required argument/); - expect(is.buildNodeParams).withArgs('foo').to.throwException(/value is a required argument/); - }); - - it('arguments should contain the provided fieldName and value as literals', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('response', 200); - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'response'); - - expect(value).to.have.property('type', 'literal'); - expect(value).to.have.property('value', 200); - }); - - it('should detect wildcards in the provided arguments', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('machine*', 'win*'); - - expect(fieldName).to.have.property('type', 'wildcard'); - expect(value).to.have.property('type', 'wildcard'); - }); - - it('should default to a non-phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200); - expect(isPhrase.value).to.be(false); - }); - - it('should allow specification of a phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200, true); - expect(isPhrase.value).to.be(true); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES match_all query when fieldName and value are both "*"', function () { - const expected = { - match_all: {} - }; - - const node = nodeTypes.function.buildNode('is', '*', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES multi_match query using default_field when fieldName is null', function () { - const expected = { - multi_match: { - query: 200, - type: 'best_fields', - lenient: true, - } - }; - - const node = nodeTypes.function.buildNode('is', null, 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', function () { - const expected = { - query_string: { - query: 'jpg*', - } - }; - - const node = nodeTypes.function.buildNode('is', null, 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES bool query with a sub-query for each field when fieldName is "*"', function () { - const node = nodeTypes.function.buildNode('is', '*', 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('bool'); - expect(result.bool.should).to.have.length(indexPattern.fields.length); - }); - - it('should return an ES exists query when value is "*"', function () { - const expected = { - bool: { - should: [ - { exists: { field: 'extension' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided without an index pattern', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node); - expect(result).to.eql(expected); - }); - - it('should support creation of phrase queries', function () { - const expected = { - bool: { - should: [ - { match_phrase: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should create a query_string query for wildcard values', function () { - const expected = { - bool: { - should: [ - { - query_string: { - fields: ['extension'], - query: 'jpg*' - } - }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support scripted fields', function () { - const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); - }); - - it('should support date fields without a dateFormat provided', function () { - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support date fields with a dateFormat provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - time_zone: 'America/Phoenix', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - it('should use a provided nested context to create a full field name', function () { - const expected = { - bool: { - should: [ - { match: { 'nestedField.extension': 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.eql(expected); - }); - - it('should support wildcard field names', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should automatically add a nested query when a wildcard field name covers a nested field', () => { - const expected = { - bool: { - should: [ - { - nested: { - path: 'nestedField.nestedChild', - query: { - match: { - 'nestedField.nestedChild.doublyNestedChild': 'foo' - } - }, - score_mode: 'none' - } - } - ], - minimum_should_match: 1 - } - }; - - - const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js b/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js deleted file mode 100644 index dae15979a161c..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { nodeTypes } from '../../../node_types'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; -import { getFullFieldNameNode } from '../../utils/get_full_field_name_node'; - -let indexPattern; - -describe('getFullFieldNameNode', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - it('should return unchanged name node if no nested path is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('notNested'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should add the nested path if it is valid according to the index pattern', () => { - const nameNode = nodeTypes.literal.buildNode('child'); - const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); - expect(result).to.eql(nodeTypes.literal.buildNode('nestedField.child')); - }); - - it('should throw an error if a path is provided for a non-nested field', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'machine') - .to - .throwException(/machine.os is not a nested field but is in nested group "machine" in the KQL expression/); - }); - - it('should throw an error if a nested field is not passed with a path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedField.child'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern) - .to - .throwException(/nestedField.child is a nested field, but is not in a nested group in the KQL expression./); - }); - - it('should throw an error if a nested field is passed with the wrong path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'nestedField') - .to - // eslint-disable-next-line max-len - .throwException(/Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/); - }); - - it('should skip error checking for wildcard names', () => { - const nameNode = nodeTypes.wildcard.buildNode('nested*'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should skip error checking if no index pattern is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, null, 'machine') - .to - .not - .throwException(); - - const result = getFullFieldNameNode(nameNode, null, 'machine'); - expect(result).to.eql(nodeTypes.literal.buildNode('machine.os')); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js deleted file mode 100644 index de00c083fc830..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as functionType from '../function'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import * as isFunction from '../../functions/is'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('function', function () { - - let indexPattern; - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNode', function () { - - it('should return a node representing the given kuery function', function () { - const result = functionType.buildNode('is', 'extension', 'jpg'); - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - }); - - }); - - describe('buildNodeWithArgumentNodes', function () { - - it('should return a function node with the given argument list untouched', function () { - const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); - const valueLiteral = nodeTypes.literal.buildNode('jpg'); - const argumentNodes = [fieldNameLiteral, valueLiteral]; - const result = functionType.buildNodeWithArgumentNodes('is', argumentNodes); - - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - expect(result.arguments).to.be(argumentNodes); - expect(result.arguments).to.eql(argumentNodes); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given function type\'s ES query representation', function () { - const node = functionType.buildNode('is', 'extension', 'jpg'); - const expected = isFunction.toElasticsearchQuery(node, indexPattern); - const result = functionType.toElasticsearchQuery(node, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); - }); - - }); - - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js deleted file mode 100644 index cfb8f6d5274db..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as namedArg from '../named_arg'; -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('named arg', function () { - - describe('buildNode', function () { - - it('should return a node representing a named argument with the given value', function () { - const result = namedArg.buildNode('fieldName', 'foo'); - expect(result).to.have.property('type', 'namedArg'); - expect(result).to.have.property('name', 'fieldName'); - expect(result).to.have.property('value'); - - const literalValue = result.value; - expect(literalValue).to.have.property('type', 'literal'); - expect(literalValue).to.have.property('value', 'foo'); - }); - - it('should support literal nodes as values', function () { - const value = nodeTypes.literal.buildNode('foo'); - const result = namedArg.buildNode('fieldName', value); - expect(result.value).to.be(value); - expect(result.value).to.eql(value); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the argument value represented by the given node', function () { - const node = namedArg.buildNode('fieldName', 'foo'); - const result = namedArg.toElasticsearchQuery(node); - expect(result).to.be('foo'); - }); - - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js deleted file mode 100644 index 0c4379378c6d6..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as wildcard from '../wildcard'; - -describe('kuery node types', function () { - - describe('wildcard', function () { - - describe('buildNode', function () { - - it('should accept a string argument representing a wildcard string', function () { - const wildcardValue = `foo${wildcard.wildcardSymbol}bar`; - const result = wildcard.buildNode(wildcardValue); - expect(result).to.have.property('type', 'wildcard'); - expect(result).to.have.property('value', wildcardValue); - }); - - it('should accept and parse a wildcard string', function () { - const result = wildcard.buildNode('foo*bar'); - expect(result).to.have.property('type', 'wildcard'); - expect(result.value).to.be(`foo${wildcard.wildcardSymbol}bar`); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toElasticsearchQuery(node); - expect(result).to.be('foo*bar'); - }); - - }); - - describe('toQueryStringQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('foo*bar'); - }); - - it('should escape query_string query special characters other than wildcard', function () { - const node = wildcard.buildNode('+foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('\\+foo*bar'); - }); - - }); - - describe('test', function () { - - it('should return a boolean indicating whether the string matches the given wildcard node', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foobar')).to.be(true); - expect(wildcard.test(node, 'foobazbar')).to.be(true); - expect(wildcard.test(node, 'foobar')).to.be(true); - - expect(wildcard.test(node, 'fooqux')).to.be(false); - expect(wildcard.test(node, 'bazbar')).to.be(false); - }); - - it('should return a true even when the string has newlines or tabs', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foo\nbar')).to.be(true); - expect(wildcard.test(node, 'foo\tbar')).to.be(true); - }); - }); - - describe('hasLeadingWildcard', function () { - it('should determine whether a wildcard node contains a leading wildcard', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.hasLeadingWildcard(node)).to.be(false); - - const leadingWildcardNode = wildcard.buildNode('*foobar'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(true); - }); - - // Lone wildcards become exists queries, so we aren't worried about their performance - it('should not consider a lone wildcard to be a leading wildcard', function () { - const leadingWildcardNode = wildcard.buildNode('*'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(false); - }); - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js deleted file mode 100644 index 6deaccadfdb76..0000000000000 --- a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { getTimeZoneFromSettings } from '../get_time_zone_from_settings'; - -describe('get timezone from settings', function () { - - it('should return the config timezone if the time zone is set', function () { - const result = getTimeZoneFromSettings('America/Chicago'); - expect(result).to.eql('America/Chicago'); - }); - - it('should return the system timezone if the time zone is set to "Browser"', function () { - const result = getTimeZoneFromSettings('Browser'); - expect(result).to.not.equal('Browser'); - }); - -}); - diff --git a/packages/kbn-es-query/src/utils/filters.js b/packages/kbn-es-query/src/utils/filters.js deleted file mode 100644 index 6e4f5c342688c..0000000000000 --- a/packages/kbn-es-query/src/utils/filters.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { pick, get, reduce, map } from 'lodash'; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const getConvertedValueForField = (field, value) => { - if (typeof value !== 'boolean' && field.type === 'boolean') { - if ([1, 'true'].includes(value)) { - return true; - } else if ([0, 'false'].includes(value)) { - return false; - } else { - throw new Error(`${value} is not a valid boolean value for boolean field ${field.name}`); - } - } - return value; -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const buildInlineScriptForPhraseFilter = (scriptedField) => { - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (scriptedField.lang === 'painless') { - return ( - `boolean compare(Supplier s, def v) {return s.get() == v;}` + - `compare(() -> { ${scriptedField.script} }, params.value);` - ); - } else { - return `(${scriptedField.script}) == value`; - } -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export function getPhraseScript(field, value) { - const convertedValue = getConvertedValueForField(field, value); - const script = buildInlineScriptForPhraseFilter(field); - - return { - script: { - source: script, - lang: field.lang, - params: { - value: convertedValue, - }, - }, - }; -} - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/range_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'kuery' into new platform - * */ -export function getRangeScript(field, params) { - const operators = { - gt: '>', - gte: '>=', - lte: '<=', - lt: '<', - }; - const comparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get() > v}', - gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}', - lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}', - lt: 'boolean lt(Supplier s, def v) {return s.get() < v}', - }; - - const dateComparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get().toInstant().isAfter(Instant.parse(v))}', - gte: 'boolean gte(Supplier s, def v) {return !s.get().toInstant().isBefore(Instant.parse(v))}', - lte: 'boolean lte(Supplier s, def v) {return !s.get().toInstant().isAfter(Instant.parse(v))}', - lt: 'boolean lt(Supplier s, def v) {return s.get().toInstant().isBefore(Instant.parse(v))}', - }; - - const knownParams = pick(params, (val, key) => { - return key in operators; - }); - let script = map(knownParams, (val, key) => { - return '(' + field.script + ')' + get(operators, key) + key; - }).join(' && '); - - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (field.lang === 'painless') { - const comp = field.type === 'date' ? dateComparators : comparators; - const currentComparators = reduce( - knownParams, - (acc, val, key) => acc.concat(get(comp, key)), - [] - ).join(' '); - - const comparisons = map(knownParams, (val, key) => { - return `${key}(() -> { ${field.script} }, params.${key})`; - }).join(' && '); - - script = `${currentComparators}${comparisons}`; - } - - return { - script: { - source: script, - params: knownParams, - lang: field.lang, - }, - }; -} diff --git a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js deleted file mode 100644 index 1a06941ece127..0000000000000 --- a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import moment from 'moment-timezone'; -const detectedTimezone = moment.tz.guess(); - -export function getTimeZoneFromSettings(dateFormatTZ) { - if (dateFormatTZ === 'Browser') { - return detectedTimezone; - } - return dateFormatTZ; -} diff --git a/packages/kbn-es-query/src/utils/index.js b/packages/kbn-es-query/src/utils/index.js deleted file mode 100644 index 27f51c1f44cf2..0000000000000 --- a/packages/kbn-es-query/src/utils/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './get_time_zone_from_settings'; diff --git a/packages/kbn-es-query/tasks/build_cli.js b/packages/kbn-es-query/tasks/build_cli.js deleted file mode 100644 index 2a43c4d10e007..0000000000000 --- a/packages/kbn-es-query/tasks/build_cli.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { resolve } = require('path'); - -const getopts = require('getopts'); -const del = require('del'); -const supportsColor = require('supports-color'); -const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -const unknownFlags = []; -const flags = getopts(process.argv, { - boolean: ['watch', 'help', 'source-maps'], - unknown(name) { - unknownFlags.push(name); - }, -}); - -const log = new ToolingLog({ - level: pickLevelFromFlags(flags), - writeTo: process.stdout, -}); - -if (unknownFlags.length) { - log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`); - flags.help = true; - process.exitCode = 1; -} - -if (flags.help) { - log.info(` - Simple build tool for @kbn/es-query package - - --watch Run in watch mode - --source-maps Include sourcemaps - --help Show this message - `); - process.exit(); -} - -withProcRunner(log, async proc => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['public', 'server'].map(subTask => - proc.run(padRight(12, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.js,.ts,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), - ], - wait: true, - cwd, - env: { - ...env, - BABEL_ENV: subTask, - }, - }) - ), - ]); - - log.success('Complete'); -}).catch(error => { - log.error(error); - process.exit(1); -}); diff --git a/packages/kbn-es-query/tsconfig.browser.json b/packages/kbn-es-query/tsconfig.browser.json deleted file mode 100644 index 4a91407471266..0000000000000 --- a/packages/kbn-es-query/tsconfig.browser.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.browser.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/public" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json deleted file mode 100644 index 05f51bbccd2ff..0000000000000 --- a/packages/kbn-es-query/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/server" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 16a634b2d3287..30a98c9046ff5 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -846,7 +846,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 4c4f321695d70..13a132ab9dd67 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { fromKueryExpression } from '@kbn/es-query'; +import { esKuery } from '../../../../../plugins/data/server'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; @@ -64,7 +64,7 @@ describe('Filter Utils', () => { test('Validate a simple filter', () => { expect( validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) - ).toEqual(fromKueryExpression('foo.title: "best"')); + ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); }); test('Assemble filter kuery node saved object attributes with one saved object type', () => { expect( @@ -74,7 +74,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -88,7 +88,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -102,7 +102,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' ) ); @@ -130,7 +130,7 @@ describe('Filter Utils', () => { describe('#validateFilterKueryNode', () => { test('Validate filter query through KueryNode - happy path', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -185,7 +185,7 @@ describe('Filter Utils', () => { test('Return Error if key is not wrapper by a saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -240,7 +240,7 @@ describe('Filter Utils', () => { test('Return Error if key of a saved object type is not wrapped with attributes', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), ['foo'], @@ -297,7 +297,7 @@ describe('Filter Utils', () => { test('Return Error if filter is not using an allowed type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -352,7 +352,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -408,7 +408,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key null key', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression('foo.attributes.description: hello AND bye'), + esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), ['foo'], mockMappings ); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index e331d3eff990f..3cf499de541ee 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -17,18 +17,18 @@ * under the License. */ -import { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; import { get, set } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; +import { esKuery } from '../../../../../plugins/data/server'; export const validateConvertFilterToKueryNode = ( allowedTypes: string[], filter: string, indexMapping: IndexMapping -): KueryNode => { +): esKuery.KueryNode | undefined => { if (filter && filter.length > 0 && indexMapping) { - const filterKueryNode = fromKueryExpression(filter); + const filterKueryNode = esKuery.fromKueryExpression(filter); const validationFilterKuery = validateFilterKueryNode( filterKueryNode, @@ -54,7 +54,7 @@ export const validateConvertFilterToKueryNode = ( validationFilterKuery.forEach(item => { const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.'); - const existingKueryNode: KueryNode = + const existingKueryNode: esKuery.KueryNode = path.length === 0 ? filterKueryNode : get(filterKueryNode, path); if (item.isSavedObjectAttr) { existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; @@ -63,8 +63,8 @@ export const validateConvertFilterToKueryNode = ( set( filterKueryNode, path, - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', 'type', itemType[0]), + esKuery.nodeTypes.function.buildNode('and', [ + esKuery.nodeTypes.function.buildNode('is', 'type', itemType[0]), existingKueryNode, ]) ); @@ -79,7 +79,6 @@ export const validateConvertFilterToKueryNode = ( }); return filterKueryNode; } - return null; }; interface ValidateFilterKueryNode { @@ -91,41 +90,44 @@ interface ValidateFilterKueryNode { } export const validateFilterKueryNode = ( - astFilter: KueryNode, + astFilter: esKuery.KueryNode, types: string[], indexMapping: IndexMapping, storeValue: boolean = false, path: string = 'arguments' ): ValidateFilterKueryNode[] => { - return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { - if (ast.arguments) { - const myPath = `${path}.${index}`; - return [ - ...kueryNode, - ...validateFilterKueryNode( - ast, - types, - indexMapping, - ast.type === 'function' && ['is', 'range'].includes(ast.function), - `${myPath}.arguments` - ), - ]; - } - if (storeValue && index === 0) { - const splitPath = path.split('.'); - return [ - ...kueryNode, - { - astPath: splitPath.slice(0, splitPath.length - 1).join('.'), - error: hasFilterKeyError(ast.value, types, indexMapping), - isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), - key: ast.value, - type: getType(ast.value), - }, - ]; - } - return kueryNode; - }, []); + return astFilter.arguments.reduce( + (kueryNode: string[], ast: esKuery.KueryNode, index: number) => { + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode( + ast, + types, + indexMapping, + ast.type === 'function' && ['is', 'range'].includes(ast.function), + `${myPath}.arguments` + ), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + return [ + ...kueryNode, + { + astPath: splitPath.slice(0, splitPath.length - 1).join('.'), + error: hasFilterKeyError(ast.value, types, indexMapping), + isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), + key: ast.value, + type: getType(ast.value), + }, + ]; + } + return kueryNode; + }, + [] + ); }; const getType = (key: string | undefined | null) => diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 79a3e573ab98c..3d81c2c2efd52 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1289,8 +1289,7 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, - indexPattern: undefined, - kueryNode: null, + kueryNode: undefined, }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 51d4a8ad50ad6..e8f1fb16461c1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -448,11 +448,11 @@ export class SavedObjectsRepository { } let kueryNode; + try { - kueryNode = - filter && filter !== '' - ? validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings) - : null; + if (filter) { + kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings); + } } catch (e) { if (e.name === 'KQLSyntaxError') { throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index bee35b899d83c..cfeb258c2f03b 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; +import { esKuery } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; @@ -91,7 +91,7 @@ interface QueryParams { searchFields?: string[]; defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } /** @@ -111,7 +111,7 @@ export function getQueryParams({ const types = getTypes(mappings, type); const bool: any = { filter: [ - ...(kueryNode != null ? [toElasticsearchQuery(kueryNode)] : []), + ...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []), { bool: { must: hasReference diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 868ca51a76eab..f2bbc3ef564a1 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -17,13 +17,13 @@ * under the License. */ -import { KueryNode } from '@kbn/es-query'; import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +import { esKuery } from '../../../../../../plugins/data/server'; interface GetSearchDslOptions { type: string | string[]; @@ -37,7 +37,7 @@ interface GetSearchDslOptions { type: string; id: string; }; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } export function getSearchDsl( diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index 1bf8ac086d341..ed3c2413b0eb4 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -18,11 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query'; - import classNames from 'classnames'; import React, { useState } from 'react'; - import { EuiButton, EuiFlexGroup, @@ -42,9 +39,9 @@ import { Query, PersistedLog, getQueryLog, + esKuery, } from '../../../../../../../plugins/data/public'; import { useKibana, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; - import { IndexPattern } from '../../../index_patterns'; import { QueryBarInput } from './query_bar_input'; @@ -300,7 +297,7 @@ function QueryBarTopRowUI(props: Props) { language === 'kuery' && typeof query === 'string' && (!storage || !storage.get('kibana.luceneSyntaxWarningOptOut')) && - doesKueryExpressionHaveLuceneSyntaxError(query) + esKuery.doesKueryExpressionHaveLuceneSyntaxError(query) ) { const toast = notifications!.toasts.addWarning({ title: intl.formatMessage({ diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts index 74111bf794877..14cd3d0083e6a 100644 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts @@ -83,7 +83,7 @@ export function getTimelionRequestHandler(dependencies: TimelionVisualizationDep sheet: [expression], extended: { es: { - filter: esQuery.buildEsQuery(null, query, filters, esQueryConfigs), + filter: esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs), }, }, time: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 2b42c22ad7c43..1d42b77336933 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { VisEditorVisualization } from './vis_editor_visualization'; import { Visualization } from './visualization'; import { VisPicker } from './vis_picker'; @@ -30,6 +29,7 @@ import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../common/extract_index_patterns'; +import { esKuery } from '../../../../../plugins/data/public'; import { npStart } from 'ui/new_platform'; @@ -88,7 +88,7 @@ export class VisEditor extends Component { if (filterQuery && filterQuery.language === 'kuery') { try { const queryOptions = this.coreContext.uiSettings.get('query:allowLeadingWildcards'); - fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); + esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); } catch (error) { return false; } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts index 83ae31bf87400..26380bf2b9d94 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts @@ -49,7 +49,7 @@ export function createVegaRequestHandler({ timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - const filtersDsl = esQuery.buildEsQuery(null, query, filters, esQueryConfigs); + const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); return vp.parseAsync(); diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts index 3db23051b6ced..405754ffcb572 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts @@ -18,7 +18,7 @@ */ import { buildEsQuery } from './build_es_query'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { luceneStringToDsl } from './lucene_string_to_dsl'; import { decorateQuery } from './decorate_query'; import { IIndexPattern } from '../../index_patterns'; diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index b754496793660..e4f5f1f9e216c 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -41,7 +41,7 @@ export interface EsQueryConfig { * config contains dateformat:tz */ export function buildEsQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query | Query[], filters: Filter | Filter[], config: EsQueryConfig = { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6e03c665290ae..669c5a62af726 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -31,9 +31,8 @@ describe('filterMatchesIndex', () => { it('should return true if no index pattern is passed', () => { const filter = { meta: { index: 'foo', key: 'bar' } } as Filter; - const indexPattern = null; - expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + expect(filterMatchesIndex(filter, undefined)).toBe(true); }); it('should return true if the filter key matches a field name', () => { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 9b68f5088c447..a9cd3d8b7ba26 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -25,7 +25,7 @@ import { Filter } from '../filters'; * this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking * change. */ -export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { +export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) { if (!filter.meta?.key || !indexPattern) { return true; } diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts index 1e0957d816590..e33040485bf47 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.ts @@ -54,7 +54,7 @@ const translateToQuery = (filter: Filter) => { export const buildQueryFromFilters = ( filters: Filter[] = [], - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex: boolean = false ) => { filters = filters.filter(filter => filter && !isFilterDisabled(filter)); diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts index 000815b51f620..4574cd5ffd0cb 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts @@ -18,7 +18,7 @@ */ import { buildQueryFromKuery } from './from_kuery'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { fields } from '../../index_patterns/mocks'; import { Query } from '../../query/types'; @@ -30,7 +30,7 @@ describe('build query', () => { describe('buildQueryFromKuery', () => { test('should return the parameters of an Elasticsearch bool query', () => { - const result = buildQueryFromKuery(null, [], true); + const result = buildQueryFromKuery(undefined, [], true); const expected = { must: [], filter: [], diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts index f91c3d97b95b4..f4ec0fe0b34c5 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts @@ -17,12 +17,12 @@ * under the License. */ -import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { Query } from '../../query/types'; export function buildQueryFromKuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query[] = [], allowLeadingWildcards: boolean = false, dateFormatTZ?: string @@ -35,22 +35,20 @@ export function buildQueryFromKuery( } function buildQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queryASTs: KueryNode[], config: Record = {} ) { - const compoundQueryAST: KueryNode = nodeTypes.function.buildNode('and', queryASTs); - const kueryQuery: Record = toElasticsearchQuery( - compoundQueryAST, - indexPattern, - config - ); + const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs); + const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config); - return { - must: [], - filter: [], - should: [], - must_not: [], - ...kueryQuery.bool, - }; + return Object.assign( + { + must: [], + filter: [], + should: [], + must_not: [], + }, + kueryQuery.bool + ); } diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts index 4617ee1a1c43d..e01240da87543 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts @@ -40,7 +40,7 @@ describe('migrateFilter', function() { } as unknown) as PhraseFilter; it('should migrate match filters of type phrase', function() { - const migratedFilter = migrateFilter(oldMatchPhraseFilter, null); + const migratedFilter = migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(migratedFilter, newMatchPhraseFilter)).toBe(true); }); @@ -48,7 +48,7 @@ describe('migrateFilter', function() { it('should not modify the original filter', function() { const oldMatchPhraseFilterCopy = clone(oldMatchPhraseFilter, true); - migrateFilter(oldMatchPhraseFilter, null); + migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(oldMatchPhraseFilter, oldMatchPhraseFilterCopy)).toBe(true); }); @@ -57,7 +57,7 @@ describe('migrateFilter', function() { const originalFilter = { match_all: {}, } as MatchAllFilter; - const migratedFilter = migrateFilter(originalFilter, null); + const migratedFilter = migrateFilter(originalFilter, undefined); expect(migratedFilter).toBe(originalFilter); expect(isEqual(migratedFilter, originalFilter)).toBe(true); diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index 258ab9e703131..fdc40768ebe41 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -43,7 +43,7 @@ function isMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter return Boolean(fieldName && get(filter, ['match', fieldName, 'type']) === 'phrase'); } -export function migrateFilter(filter: Filter, indexPattern: IIndexPattern | null) { +export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) { if (isMatchPhraseFilter(filter)) { const fieldName = Object.keys(filter.match)[0]; const params: Record = get(filter, ['match', fieldName]); diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 56eb45c4b1dca..937fe09903b6b 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -18,6 +18,7 @@ */ import * as esQuery from './es_query'; import * as esFilters from './filters'; +import * as esKuery from './kuery'; import * as utils from './utils'; -export { esFilters, esQuery, utils }; +export { esFilters, esQuery, utils, esKuery }; diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.js rename to src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts new file mode 100644 index 0000000000000..e441420760475 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -0,0 +1,421 @@ +/* + * 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 { + fromKueryExpression, + fromLiteralExpression, + toElasticsearchQuery, + doesKueryExpressionHaveLuceneSyntaxError, +} from './ast'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode } from '../types'; + +describe('kuery AST API', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('fromKueryExpression', () => { + test('should return a match all "is" function for whitespace', () => { + const expected = nodeTypes.function.buildNode('is', '*', '*'); + const actual = fromKueryExpression(' '); + expect(actual).toEqual(expected); + }); + + test('should return an "is" function with a null field for single literals', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression('foo'); + expect(actual).toEqual(expected); + }); + + test('should ignore extraneous whitespace at the beginning and end of the query', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression(' foo '); + expect(actual).toEqual(expected); + }); + + test('should not split on whitespace', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); + const actual = fromKueryExpression('foo bar'); + expect(actual).toEqual(expected); + }); + + test('should support "and" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo and bar'); + expect(actual).toEqual(expected); + }); + + test('should support "or" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo or bar'); + expect(actual).toEqual(expected); + }); + + test('should support negation of queries with a "not" prefix', () => { + const expected = nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]) + ); + const actual = fromKueryExpression('not (foo or bar)'); + expect(actual).toEqual(expected); + }); + + test('"and" should have a higher precedence than "or"', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'bar'), + nodeTypes.function.buildNode('is', null, 'baz'), + ]), + nodeTypes.function.buildNode('is', null, 'qux'), + ]), + ]); + const actual = fromKueryExpression('foo or bar and baz or qux'); + expect(actual).toEqual(expected); + }); + + test('should support grouping to override default precedence', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]), + nodeTypes.function.buildNode('is', null, 'baz'), + ]); + const actual = fromKueryExpression('(foo or bar) and baz'); + expect(actual).toEqual(expected); + }); + + test('should support matching against specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); + const actual = fromKueryExpression('foo:bar'); + expect(actual).toEqual(expected); + }); + + test('should also not split on whitespace when matching specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); + const actual = fromKueryExpression('foo:bar baz'); + expect(actual).toEqual(expected); + }); + + test('should treat quoted values as phrases', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); + const actual = fromKueryExpression('foo:"bar baz"'); + expect(actual).toEqual(expected); + }); + + test('should support a shorthand for matching multiple values against a single field', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]); + const actual = fromKueryExpression('foo:(bar or baz)'); + expect(actual).toEqual(expected); + }); + + test('should support "and" and "not" operators and grouping in the shorthand as well', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]), + nodeTypes.function.buildNode('not', nodeTypes.function.buildNode('is', 'foo', 'qux')), + ]); + const actual = fromKueryExpression('foo:((bar or baz) and not qux)'); + expect(actual).toEqual(expected); + }); + + test('should support exclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gt: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lt: 8000, + }), + ]); + const actual = fromKueryExpression('bytes > 1000 and bytes < 8000'); + expect(actual).toEqual(expected); + }); + + test('should support inclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gte: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lte: 8000, + }), + ]); + const actual = fromKueryExpression('bytes >= 1000 and bytes <= 8000'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in field names', () => { + const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); + const actual = fromKueryExpression('machine*:osx'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in values', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); + const actual = fromKueryExpression('foo:ba*'); + expect(actual).toEqual(expected); + }); + + test('should create an exists "is" query when a field is given and "*" is the value', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', '*'); + const actual = fromKueryExpression('foo:*'); + expect(actual).toEqual(expected); + }); + + test('should support nested queries indicated by curly braces', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ); + const actual = fromKueryExpression('nestedField:{ childOfNested: foo }'); + expect(actual).toEqual(expected); + }); + + test('should support nested subqueries and subqueries inside nested queries', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), + nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), + ]) + ), + ]); + const actual = fromKueryExpression( + 'response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested sub-queries inside paren groups', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'bar') + ), + ]), + ]); + const actual = fromKueryExpression( + 'response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested groups inside other nested groups', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode( + 'nested', + 'nestedChild', + nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') + ) + ); + const actual = fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); + expect(actual).toEqual(expected); + }); + }); + + describe('fromLiteralExpression', () => { + test('should create literal nodes for unquoted values with correct primitive types', () => { + const stringLiteral = nodeTypes.literal.buildNode('foo'); + const booleanFalseLiteral = nodeTypes.literal.buildNode(false); + const booleanTrueLiteral = nodeTypes.literal.buildNode(true); + const numberLiteral = nodeTypes.literal.buildNode(42); + + expect(fromLiteralExpression('foo')).toEqual(stringLiteral); + expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); + expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); + expect(fromLiteralExpression('42')).toEqual(numberLiteral); + }); + + test('should allow escaping of special characters with a backslash', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + // yo dawg + const actual = fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); + expect(actual).toEqual(expected); + }); + + test('should support double quoted strings that do not need escapes except for quotes', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + const actual = fromLiteralExpression('"\\():<>\\"*"'); + expect(actual).toEqual(expected); + }); + + test('should support escaped backslashes inside quoted strings', () => { + const expected = nodeTypes.literal.buildNode('\\'); + const actual = fromLiteralExpression('"\\\\"'); + expect(actual).toEqual(expected); + }); + + test('should detect wildcards and build wildcard AST nodes', () => { + const expected = nodeTypes.wildcard.buildNode('foo*bar'); + const actual = fromLiteralExpression('foo*bar'); + expect(actual).toEqual(expected); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given node type's ES query representation", () => { + const node = nodeTypes.function.buildNode('exists', 'response'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + expect(result).toEqual(expected); + }); + + test('should return an empty "and" function for undefined nodes and unknown node types', () => { + const expected = nodeTypes.function.toElasticsearchQuery( + nodeTypes.function.buildNode('and', []), + indexPattern + ); + + expect(toElasticsearchQuery((null as unknown) as KueryNode, undefined)).toEqual(expected); + + const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + delete noTypeNode.type; + expect(toElasticsearchQuery(noTypeNode)).toEqual(expected); + + const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + + // @ts-ignore + unknownTypeNode.type = 'notValid'; + expect(toElasticsearchQuery(unknownTypeNode)).toEqual(expected); + }); + + test("should return the given node type's ES query representation including a time zone parameter when one is provided", () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); + const result = toElasticsearchQuery(node, indexPattern, config); + expect(result).toEqual(expected); + }); + }); + + describe('doesKueryExpressionHaveLuceneSyntaxError', () => { + test('should return true for Lucene ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); + expect(result).toEqual(true); + }); + + test('should return false for KQL ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); + expect(result).toEqual(true); + }); + + test('should return false for KQL exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar:*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); + expect(result).toEqual(true); + }); + + test('should return false for KQL wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene regex', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene fuzziness', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene proximity', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene boosting', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene + operator', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene - operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene && operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene || operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for mixed KQL/Lucene queries', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); + expect(result).toEqual(true); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.js b/src/plugins/data/common/es_query/kuery/ast/ast.ts similarity index 53% rename from packages/kbn-es-query/src/kuery/ast/ast.js rename to src/plugins/data/common/es_query/kuery/ast/ast.ts index 1688995d46f80..253f432617972 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.js +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -17,21 +17,44 @@ * under the License. */ -import _ from 'lodash'; import { nodeTypes } from '../node_types/index'; -import { parse as parseKuery } from './kuery'; -import { KQLSyntaxError } from '../errors'; +import { KQLSyntaxError } from '../kuery_syntax_error'; +import { KueryNode, JsonObject, DslQuery, KueryParseOptions } from '../types'; +import { IIndexPattern } from '../../../index_patterns/types'; -export function fromLiteralExpression(expression, parseOptions) { - parseOptions = { - ...parseOptions, - startRule: 'Literal', - }; +// @ts-ignore +import { parse as parseKuery } from './_generated_/kuery'; - return fromExpression(expression, parseOptions, parseKuery); -} +const fromExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {}, + parse: Function = parseKuery +): KueryNode => { + if (typeof expression === 'undefined') { + throw new Error('expression must be a string, got undefined instead'); + } + + return parse(expression, { ...parseOptions, helpers: { nodeTypes } }); +}; + +export const fromLiteralExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { + return fromExpression( + expression, + { + ...parseOptions, + startRule: 'Literal', + }, + parseKuery + ); +}; -export function fromKueryExpression(expression, parseOptions) { +export const fromKueryExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { try { return fromExpression(expression, parseOptions, parseKuery); } catch (error) { @@ -41,20 +64,18 @@ export function fromKueryExpression(expression, parseOptions) { throw error; } } -} +}; -function fromExpression(expression, parseOptions = {}, parse = parseKuery) { - if (_.isUndefined(expression)) { - throw new Error('expression must be a string, got undefined instead'); +export const doesKueryExpressionHaveLuceneSyntaxError = ( + expression: string | DslQuery +): boolean => { + try { + fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); + return false; + } catch (e) { + return e.message.startsWith('Lucene'); } - - parseOptions = { - ...parseOptions, - helpers: { nodeTypes }, - }; - - return parse(expression, parseOptions); -} +}; /** * @params {String} indexPattern @@ -63,19 +84,17 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) { * IndexPattern isn't required, but if you pass one in, we can be more intelligent * about how we craft the queries (e.g. scripted fields) */ -export function toElasticsearchQuery(node, indexPattern, config = {}, context = {}) { +export const toElasticsearchQuery = ( + node: KueryNode, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record +): JsonObject => { if (!node || !node.type || !nodeTypes[node.type]) { - return toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); + return toElasticsearchQuery(nodeTypes.function.buildNode('and', []), indexPattern); } - return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config, context); -} + const nodeType = (nodeTypes[node.type] as unknown) as any; -export function doesKueryExpressionHaveLuceneSyntaxError(expression) { - try { - fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); - return false; - } catch (e) { - return (e.message.startsWith('Lucene')); - } -} + return nodeType.toElasticsearchQuery(node, indexPattern, config, context); +}; diff --git a/packages/kbn-es-query/src/kuery/ast/index.js b/src/plugins/data/common/es_query/kuery/ast/index.ts similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/index.js rename to src/plugins/data/common/es_query/kuery/ast/index.ts diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.peg rename to src/plugins/data/common/es_query/kuery/ast/kuery.peg diff --git a/packages/kbn-es-query/src/kuery/functions/and.js b/src/plugins/data/common/es_query/kuery/functions/and.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js b/src/plugins/data/common/es_query/kuery/functions/and.test.ts similarity index 50% rename from packages/kbn-es-query/src/kuery/functions/__tests__/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.test.ts index 07289a878e8c1..133e691b27dba 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js +++ b/src/plugins/data/common/es_query/kuery/functions/and.test.ts @@ -17,43 +17,53 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as and from '../and'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import * as ast from '../ast'; -let indexPattern; +// @ts-ignore +import * as and from './and'; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); -describe('kuery functions', function () { - describe('and', function () { +describe('kuery functions', () => { + describe('and', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = and.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s filter clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's filter clause", () => { const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]); const result = and.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('filter'); - expect(result.bool.filter).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('filter'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.filter).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) ); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts similarity index 51% rename from packages/kbn-es-query/src/kuery/functions/__tests__/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.test.ts index ee4cfab94e614..8443436cf4cfb 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js +++ b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts @@ -17,67 +17,73 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as exists from '../exists'; -import { nodeTypes } from '../../node_types'; -import _ from 'lodash'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +// @ts-ignore +import * as exists from './exists'; -let indexPattern; - -describe('kuery functions', function () { - describe('exists', function () { +describe('kuery functions', () => { + describe('exists', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - it('should return a single "arguments" param', function () { + describe('buildNodeParams', () => { + test('should return a single "arguments" param', () => { const result = exists.buildNodeParams('response'); - expect(result).to.only.have.key('arguments'); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); }); - it('arguments should contain the provided fieldName as a literal', function () { - const { arguments: [ arg ] } = exists.buildNodeParams('response'); - expect(arg).to.have.property('type', 'literal'); - expect(arg).to.have.property('value', 'response'); + test('arguments should contain the provided fieldName as a literal', () => { + const { + arguments: [arg], + } = exists.buildNodeParams('response'); + + expect(arg).toHaveProperty('type', 'literal'); + expect(arg).toHaveProperty('value', 'response'); }); }); - describe('toElasticsearchQuery', function () { - it('should return an ES exists query', function () { + describe('toElasticsearchQuery', () => { + test('should return an ES exists query', () => { const expected = { - exists: { field: 'response' } + exists: { field: 'response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery(existsNode, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); - it('should return an ES exists query without an index pattern', function () { + test('should return an ES exists query without an index pattern', () => { const expected = { - exists: { field: 'response' } + exists: { field: 'response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery(existsNode); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); - it('should throw an error for scripted fields', function () { + test('should throw an error for scripted fields', () => { const existsNode = nodeTypes.function.buildNode('exists', 'script string'); - expect(exists.toElasticsearchQuery) - .withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/); + expect(() => exists.toElasticsearchQuery(existsNode, indexPattern)).toThrowError( + /Exists query does not support scripted fields/ + ); }); - it('should use a provided nested context to create a full field name', function () { + test('should use a provided nested context to create a full field name', () => { const expected = { - exists: { field: 'nestedField.response' } + exists: { field: 'nestedField.response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery( existsNode, @@ -85,7 +91,8 @@ describe('kuery functions', function () { {}, { nested: { path: 'nestedField' } } ); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js rename to src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts new file mode 100644 index 0000000000000..cf287ff2c437a --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoBoundingBox from './geo_bounding_box'; + +const params = { + bottomRight: { + lat: 50.73, + lon: -135.35, + }, + topLeft: { + lat: 73.12, + lon: -174.37, + }, +}; + +describe('kuery functions', () => { + describe('geoBoundingBox', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided params as named arguments with "lat, lon" string values', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [, ...args], + } = result; + + args.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['bottomRight', 'topLeft'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + + const { lat, lon } = get(params, param.name); + + expect(param.value.value).toBe(`${lat}, ${lon}`); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_bounding_box query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should return an ES geo_bounding_box query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_bounding_box.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); + + expect(() => geoBoundingBox.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo bounding box query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_polygon.js b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_polygon.js rename to src/plugins/data/common/es_query/kuery/functions/geo_polygon.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts new file mode 100644 index 0000000000000..84500cb4ade7e --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoPolygon from './geo_polygon'; + +const points = [ + { + lat: 69.77, + lon: -171.56, + }, + { + lat: 50.06, + lon: -169.1, + }, + { + lat: 69.16, + lon: -125.85, + }, +]; + +describe('kuery functions', () => { + describe('geoPolygon', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoPolygon.buildNodeParams('geo', points); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided points literal "lat, lon" string values', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [, ...args], + } = result; + + args.forEach((param: any, index: number) => { + const expectedPoint = points[index]; + const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; + + expect(param).toHaveProperty('type', 'literal'); + expect(param.value).toBe(expectedLatLon); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_polygon query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should return an ES geo_polygon query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_polygon.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); + expect(() => geoPolygon.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo polygon query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/index.js b/src/plugins/data/common/es_query/kuery/functions/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/index.js rename to src/plugins/data/common/es_query/kuery/functions/index.js diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/src/plugins/data/common/es_query/kuery/functions/is.js similarity index 95% rename from packages/kbn-es-query/src/kuery/functions/is.js rename to src/plugins/data/common/es_query/kuery/functions/is.js index 63ade9e8793a7..4f2f298c4707d 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ b/src/plugins/data/common/es_query/kuery/functions/is.js @@ -17,20 +17,22 @@ * under the License. */ -import _ from 'lodash'; -import * as ast from '../ast'; -import * as literal from '../node_types/literal'; -import * as wildcard from '../node_types/wildcard'; -import { getPhraseScript } from '../../utils/filters'; +import { get, isUndefined } from 'lodash'; +import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; +import * as ast from '../ast'; + +import * as literal from '../node_types/literal'; +import * as wildcard from '../node_types/wildcard'; + export function buildNodeParams(fieldName, value, isPhrase = false) { - if (_.isUndefined(fieldName)) { + if (isUndefined(fieldName)) { throw new Error('fieldName is a required argument'); } - if (_.isUndefined(value)) { + if (isUndefined(value)) { throw new Error('value is a required argument'); } const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); @@ -45,7 +47,7 @@ export function toElasticsearchQuery(node, indexPattern = null, config = {}, con const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); const fieldName = ast.toElasticsearchQuery(fullFieldNameArg); - const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; + const value = !isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; const type = isPhraseArg.value ? 'phrase' : 'best_fields'; if (fullFieldNameArg.value === null) { if (valueArg.type === 'wildcard') { @@ -94,7 +96,7 @@ export function toElasticsearchQuery(node, indexPattern = null, config = {}, con // users handle this themselves so we automatically add nested queries in this scenario. if ( !(fullFieldNameArg.type === 'wildcard') - || !_.get(field, 'subType.nested') + || !get(field, 'subType.nested') || context.nested ) { return query; diff --git a/src/plugins/data/common/es_query/kuery/functions/is.test.ts b/src/plugins/data/common/es_query/kuery/functions/is.test.ts new file mode 100644 index 0000000000000..df147bad54a34 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/is.test.ts @@ -0,0 +1,305 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; + +// @ts-ignore +import * as is from './is'; +import { IIndexPattern } from '../../../index_patterns'; + +describe('kuery functions', () => { + describe('is', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('fieldName and value should be required arguments', () => { + expect(() => is.buildNodeParams()).toThrowError(/fieldName is a required argument/); + expect(() => is.buildNodeParams('foo')).toThrowError(/value is a required argument/); + }); + + test('arguments should contain the provided fieldName and value as literals', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('response', 200); + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'response'); + expect(value).toHaveProperty('type', 'literal'); + expect(value).toHaveProperty('value', 200); + }); + + test('should detect wildcards in the provided arguments', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('machine*', 'win*'); + + expect(fieldName).toHaveProperty('type', 'wildcard'); + expect(value).toHaveProperty('type', 'wildcard'); + }); + + test('should default to a non-phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200); + expect(isPhrase.value).toBe(false); + }); + + test('should allow specification of a phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200, true); + expect(isPhrase.value).toBe(true); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES match_all query when fieldName and value are both "*"', () => { + const expected = { + match_all: {}, + }; + const node = nodeTypes.function.buildNode('is', '*', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES multi_match query using default_field when fieldName is null', () => { + const expected = { + multi_match: { + query: 200, + type: 'best_fields', + lenient: true, + }, + }; + const node = nodeTypes.function.buildNode('is', null, 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', () => { + const expected = { + query_string: { + query: 'jpg*', + }, + }; + const node = nodeTypes.function.buildNode('is', null, 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES bool query with a sub-query for each field when fieldName is "*"', () => { + const node = nodeTypes.function.buildNode('is', '*', 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(result.bool.should.length).toBe(indexPattern.fields.length); + }); + + test('should return an ES exists query when value is "*"', () => { + const expected = { + bool: { + should: [{ exists: { field: 'extension' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided without an index pattern', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node); + + expect(result).toEqual(expected); + }); + + test('should support creation of phrase queries', () => { + const expected = { + bool: { + should: [{ match_phrase: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should create a query_string query for wildcard values', () => { + const expected = { + bool: { + should: [ + { + query_string: { + fields: ['extension'], + query: 'jpg*', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support scripted fields', () => { + const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result.bool.should[0]).toHaveProperty('script'); + }); + + test('should support date fields without a dateFormat provided', () => { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support date fields with a dateFormat provided', () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern, config); + + expect(result).toEqual(expected); + }); + + test('should use a provided nested context to create a full field name', () => { + const expected = { + bool: { + should: [{ match: { 'nestedField.extension': 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toEqual(expected); + }); + + test('should support wildcard field names', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { + const expected = { + bool: { + should: [ + { + nested: { + path: 'nestedField.nestedChild', + query: { + match: { + 'nestedField.nestedChild.doublyNestedChild': 'foo', + }, + }, + score_mode: 'none', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/functions/__tests__/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.test.ts index 5ba73e485ddf1..945a36d304a05 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js +++ b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts @@ -17,52 +17,60 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as nested from '../nested'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; + +// @ts-ignore +import * as nested from './nested'; const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); -describe('kuery functions', function () { - describe('nested', function () { +describe('kuery functions', () => { + describe('nested', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = nested.buildNodeParams('nestedField', childNode); - const { arguments: [ resultPath, resultChildNode ] } = result; - expect(ast.toElasticsearchQuery(resultPath)).to.be('nestedField'); - expect(resultChildNode).to.be(childNode); + const { + arguments: [resultPath, resultChildNode], + } = result; + + expect(ast.toElasticsearchQuery(resultPath)).toBe('nestedField'); + expect(resultChildNode).toBe(childNode); }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES nested query', function () { + describe('toElasticsearchQuery', () => { + test('should wrap subqueries in an ES nested query', () => { const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); const result = nested.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('nested'); - expect(result.nested.path).to.be('nestedField'); - expect(result.nested.score_mode).to.be('none'); + + expect(result).toHaveProperty('nested'); + expect(Object.keys(result).length).toBe(1); + + expect(result.nested.path).toBe('nestedField'); + expect(result.nested.score_mode).toBe('none'); }); - it('should pass the nested path to subqueries so the full field name can be used', function () { + test('should pass the nested path to subqueries so the full field name can be used', () => { const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); const result = nested.toElasticsearchQuery(node, indexPattern); const expectedSubQuery = ast.toElasticsearchQuery( nodeTypes.function.buildNode('is', 'nestedField.child', 'foo') ); - expect(result.nested.query).to.eql(expectedSubQuery); - }); + expect(result.nested.query).toEqual(expectedSubQuery); + }); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/not.js b/src/plugins/data/common/es_query/kuery/functions/not.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js b/src/plugins/data/common/es_query/kuery/functions/not.test.ts similarity index 50% rename from packages/kbn-es-query/src/kuery/functions/__tests__/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.test.ts index 7a2d7fa39c152..01c1976b939ea 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js +++ b/src/plugins/data/common/es_query/kuery/functions/not.test.ts @@ -17,44 +17,50 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as not from '../not'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; -const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); +// @ts-ignore +import * as not from './not'; -describe('kuery functions', function () { +const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - describe('not', function () { +describe('kuery functions', () => { + describe('not', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child node', () => { + const { + arguments: [actualChild], + } = not.buildNodeParams(childNode); - it('arguments should contain the unmodified child node', function () { - const { arguments: [ actualChild ] } = not.buildNodeParams(childNode); - expect(actualChild).to.be(childNode); + expect(actualChild).toBe(childNode); }); - - }); - describe('toElasticsearchQuery', function () { - - it('should wrap a subquery in an ES bool query\'s must_not clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap a subquery in an ES bool query's must_not clause", () => { const node = nodeTypes.function.buildNode('not', childNode); const result = not.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('must_not'); - expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern)); - }); + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + + expect(result.bool).toHaveProperty('must_not'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.must_not).toEqual(ast.toElasticsearchQuery(childNode, indexPattern)); + }); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/or.js b/src/plugins/data/common/es_query/kuery/functions/or.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js b/src/plugins/data/common/es_query/kuery/functions/or.test.ts similarity index 52% rename from packages/kbn-es-query/src/kuery/functions/__tests__/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.test.ts index f24f24b98e7fb..a6590546e5fc5 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js +++ b/src/plugins/data/common/es_query/kuery/functions/or.test.ts @@ -17,56 +17,61 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as or from '../or'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; + +// @ts-ignore +import * as or from './or'; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); -describe('kuery functions', function () { - - describe('or', function () { +describe('kuery functions', () => { + describe('or', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = or.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); - }); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); + }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s should clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's should clause", () => { const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); const result = or.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.have.keys('should'); - expect(result.bool.should).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('should'); + expect(result.bool.should).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) ); }); - it('should require one of the clauses to match', function () { + test('should require one of the clauses to match', () => { const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); const result = or.toElasticsearchQuery(node, indexPattern); - expect(result.bool).to.have.property('minimum_should_match', 1); - }); + expect(result.bool).toHaveProperty('minimum_should_match', 1); + }); }); - }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/range.js b/src/plugins/data/common/es_query/kuery/functions/range.js similarity index 98% rename from packages/kbn-es-query/src/kuery/functions/range.js rename to src/plugins/data/common/es_query/kuery/functions/range.js index f7719998ad524..80181cfc003f1 100644 --- a/packages/kbn-es-query/src/kuery/functions/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { getRangeScript } from '../../utils/filters'; +import { getRangeScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js b/src/plugins/data/common/es_query/kuery/functions/range.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/functions/__tests__/range.js rename to src/plugins/data/common/es_query/kuery/functions/range.test.ts index 2361e8bb66769..ed8e40830df02 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.test.ts @@ -17,53 +17,57 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as range from '../range'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { RangeFilterParams } from '../../filters'; -let indexPattern; - -describe('kuery functions', function () { - - describe('range', function () { +// @ts-ignore +import * as range from './range'; +describe('kuery functions', () => { + describe('range', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the provided fieldName as a literal', function () { + describe('buildNodeParams', () => { + test('arguments should contain the provided fieldName as a literal', () => { const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 }); - const { arguments: [fieldName] } = result; + const { + arguments: [fieldName], + } = result; - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'bytes'); + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'bytes'); }); - it('arguments should contain the provided params as named arguments', function () { - const givenParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; + test('arguments should contain the provided params as named arguments', () => { + const givenParams: RangeFilterParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; const result = range.buildNodeParams('bytes', givenParams); - const { arguments: [, ...params] } = result; + const { + arguments: [, ...params], + } = result; - expect(params).to.be.an('array'); - expect(params).to.not.be.empty(); + expect(Array.isArray(params)).toBeTruthy(); + expect(params.length).toBeGreaterThan(1); - params.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['gt', 'lt', 'format'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - expect(param.value.value).to.be(givenParams[param.name]); + params.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['gt', 'lt', 'format'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + expect(param.value.value).toBe(get(givenParams, param.name)); }); }); - }); - describe('toElasticsearchQuery', function () { - - it('should return an ES range query for the node\'s field and params', function () { + describe('toElasticsearchQuery', () => { + test("should return an ES range query for the node's field and params", () => { const expected = { bool: { should: [ @@ -71,21 +75,21 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should return an ES range query without an index pattern', function () { + test('should return an ES range query without an index pattern', () => { const expected = { bool: { should: [ @@ -93,21 +97,22 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support wildcard field names', function () { + test('should support wildcard field names', () => { const expected = { bool: { should: [ @@ -115,27 +120,29 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; const node = nodeTypes.function.buildNode('range', 'byt*', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support scripted fields', function () { + test('should support scripted fields', () => { const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); + + expect(result.bool.should[0]).toHaveProperty('script'); }); - it('should support date fields without a dateFormat provided', function () { + test('should support date fields without a dateFormat provided', () => { const expected = { bool: { should: [ @@ -144,20 +151,23 @@ describe('kuery functions', function () { '@timestamp': { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17', - } - } - } + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support date fields with a dateFormat provided', function () { + test('should support date fields with a dateFormat provided', () => { const config = { dateFormatTZ: 'America/Phoenix' }; const expected = { bool: { @@ -168,20 +178,23 @@ describe('kuery functions', function () { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17', time_zone: 'America/Phoenix', - } - } - } + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); const result = range.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should use a provided nested context to create a full field name', function () { + test('should use a provided nested context to create a full field name', () => { const expected = { bool: { should: [ @@ -189,15 +202,14 @@ describe('kuery functions', function () { range: { 'nestedField.bytes': { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery( node, @@ -205,10 +217,11 @@ describe('kuery functions', function () { {}, { nested: { path: 'nestedField' } } ); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should automatically add a nested query when a wildcard field name covers a nested field', function () { + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { const expected = { bool: { should: [ @@ -219,21 +232,24 @@ describe('kuery functions', function () { range: { 'nestedField.nestedChild.doublyNestedChild': { gt: 1000, - lt: 8000 - } - } + lt: 8000, + }, + }, }, - score_mode: 'none' - } - } + score_mode: 'none', + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '*doublyNested*', { gt: 1000, lt: 8000 }); + const node = nodeTypes.function.buildNode('range', '*doublyNested*', { + gt: 1000, + lt: 8000, + }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts similarity index 52% rename from packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts index 7718479130a8a..d48f0943082c9 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts @@ -17,39 +17,41 @@ * under the License. */ -import { getFields } from '../../utils/get_fields'; -import expect from '@kbn/expect'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; +import { fields } from '../../../../index_patterns/mocks'; -import { nodeTypes } from '../../..'; +import { nodeTypes } from '../../index'; +import { IIndexPattern, IFieldType } from '../../../../index_patterns'; -let indexPattern; - -describe('getFields', function () { +// @ts-ignore +import { getFields } from './get_fields'; +describe('getFields', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('field names without a wildcard', function () { - - it('should return an empty array if the field does not exist in the index pattern', function () { + describe('field names without a wildcard', () => { + test('should return an empty array if the field does not exist in the index pattern', () => { const fieldNameNode = nodeTypes.literal.buildNode('nonExistentField'); - const expected = []; const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); + + expect(actual).toEqual([]); }); - it('should return the single matching field in an array', function () { + test('should return the single matching field in an array', () => { const fieldNameNode = nodeTypes.literal.buildNode('extension'); const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('extension'); + + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('extension'); }); - it('should not match a wildcard in a literal node', function () { + test('should not match a wildcard in a literal node', () => { const indexPatternWithWildField = { title: 'wildIndex', fields: [ @@ -61,37 +63,32 @@ describe('getFields', function () { const fieldNameNode = nodeTypes.literal.buildNode('foo*'); const results = getFields(fieldNameNode, indexPatternWithWildField); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('foo*'); - // ensure the wildcard is not actually being parsed - const expected = []; + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('foo*'); + const actual = getFields(nodeTypes.literal.buildNode('fo*'), indexPatternWithWildField); - expect(actual).to.eql(expected); + expect(actual).toEqual([]); }); }); - describe('field name patterns with a wildcard', function () { - - it('should return an empty array if it does not match any fields in the index pattern', function () { + describe('field name patterns with a wildcard', () => { + test('should return an empty array if test does not match any fields in the index pattern', () => { const fieldNameNode = nodeTypes.wildcard.buildNode('nonExistent*'); - const expected = []; const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); + + expect(actual).toEqual([]); }); - it('should return all fields that match the pattern in an array', function () { + test('should return all fields that match the pattern in an array', () => { const fieldNameNode = nodeTypes.wildcard.buildNode('machine*'); const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(2); - expect(results.find((field) => { - return field.name === 'machine.os'; - })).to.be.ok(); - expect(results.find((field) => { - return field.name === 'machine.os.raw'; - })).to.be.ok(); + + expect(Array.isArray(results)).toBeTruthy(); + expect(results).toHaveLength(2); + expect(results.find((field: IFieldType) => field.name === 'machine.os')).toBeDefined(); + expect(results.find((field: IFieldType) => field.name === 'machine.os.raw')).toBeDefined(); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts new file mode 100644 index 0000000000000..e138e22b76ad3 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { nodeTypes } from '../../node_types'; +import { fields } from '../../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../../index_patterns'; + +// @ts-ignore +import { getFullFieldNameNode } from './get_full_field_name_node'; + +describe('getFullFieldNameNode', function() { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + test('should return unchanged name node if no nested path is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('notNested'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should add the nested path if test is valid according to the index pattern', () => { + const nameNode = nodeTypes.literal.buildNode('child'); + const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); + + expect(result).toEqual(nodeTypes.literal.buildNode('nestedField.child')); + }); + + test('should throw an error if a path is provided for a non-nested field', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'machine')).toThrowError( + /machine.os is not a nested field but is in nested group "machine" in the KQL expression/ + ); + }); + + test('should throw an error if a nested field is not passed with a path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedField.child'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern)).toThrowError( + /nestedField.child is a nested field, but is not in a nested group in the KQL expression./ + ); + }); + + test('should throw an error if a nested field is passed with the wrong path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'nestedField')).toThrowError( + /Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/ + ); + }); + + test('should skip error checking for wildcard names', () => { + const nameNode = nodeTypes.wildcard.buildNode('nested*'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should skip error checking if no index pattern is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, null, 'machine')).not.toThrowError(); + + const result = getFullFieldNameNode(nameNode, null, 'machine'); + expect(result).toEqual(nodeTypes.literal.buildNode('machine.os')); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/index.js b/src/plugins/data/common/es_query/kuery/index.ts similarity index 91% rename from packages/kbn-es-query/src/kuery/index.js rename to src/plugins/data/common/es_query/kuery/index.ts index e0cacada7f274..4184dea62ef2c 100644 --- a/packages/kbn-es-query/src/kuery/index.js +++ b/src/plugins/data/common/es_query/kuery/index.ts @@ -17,6 +17,8 @@ * under the License. */ -export * from './ast'; +export { KQLSyntaxError } from './kuery_syntax_error'; export { nodeTypes } from './node_types'; -export * from './errors'; +export * from './ast'; + +export * from './types'; diff --git a/packages/kbn-es-query/src/kuery/errors/index.test.js b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts similarity index 66% rename from packages/kbn-es-query/src/kuery/errors/index.test.js rename to src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts index d8040e464b696..cfe2f86e813ca 100644 --- a/packages/kbn-es-query/src/kuery/errors/index.test.js +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts @@ -17,89 +17,92 @@ * under the License. */ -import { fromKueryExpression } from '../ast'; - +import { fromKueryExpression } from './ast'; describe('kql syntax errors', () => { - it('should throw an error for a field query missing a value', () => { expect(() => { fromKueryExpression('response:'); - }).toThrow('Expected "(", "{", value, whitespace but end of input found.\n' + - 'response:\n' + - '---------^'); + }).toThrow( + 'Expected "(", "{", value, whitespace but end of input found.\n' + + 'response:\n' + + '---------^' + ); }); it('should throw an error for an OR query missing a right side sub-query', () => { expect(() => { fromKueryExpression('response:200 or '); - }).toThrow('Expected "(", NOT, field name, value but end of input found.\n' + - 'response:200 or \n' + - '----------------^'); + }).toThrow( + 'Expected "(", NOT, field name, value but end of input found.\n' + + 'response:200 or \n' + + '----------------^' + ); }); it('should throw an error for an OR list of values missing a right side sub-query', () => { expect(() => { fromKueryExpression('response:(200 or )'); - }).toThrow('Expected "(", NOT, value but ")" found.\n' + - 'response:(200 or )\n' + - '-----------------^'); + }).toThrow( + 'Expected "(", NOT, value but ")" found.\n' + 'response:(200 or )\n' + '-----------------^' + ); }); it('should throw an error for a NOT query missing a sub-query', () => { expect(() => { fromKueryExpression('response:200 and not '); - }).toThrow('Expected "(", field name, value but end of input found.\n' + - 'response:200 and not \n' + - '---------------------^'); + }).toThrow( + 'Expected "(", field name, value but end of input found.\n' + + 'response:200 and not \n' + + '---------------------^' + ); }); it('should throw an error for a NOT list missing a sub-query', () => { expect(() => { fromKueryExpression('response:(200 and not )'); - }).toThrow('Expected "(", value but ")" found.\n' + - 'response:(200 and not )\n' + - '----------------------^'); + }).toThrow( + 'Expected "(", value but ")" found.\n' + + 'response:(200 and not )\n' + + '----------------------^' + ); }); it('should throw an error for unbalanced quotes', () => { expect(() => { fromKueryExpression('foo:"ba '); - }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + - 'foo:"ba \n' + - '----^'); + }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + 'foo:"ba \n' + '----^'); }); it('should throw an error for unescaped quotes in a quoted string', () => { expect(() => { fromKueryExpression('foo:"ba "r"'); - }).toThrow('Expected AND, OR, end of input, whitespace but "r" found.\n' + - 'foo:"ba "r"\n' + - '---------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but "r" found.\n' + 'foo:"ba "r"\n' + '---------^' + ); }); it('should throw an error for unescaped special characters in literals', () => { expect(() => { fromKueryExpression('foo:ba:r'); - }).toThrow('Expected AND, OR, end of input, whitespace but ":" found.\n' + - 'foo:ba:r\n' + - '------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but ":" found.\n' + 'foo:ba:r\n' + '------^' + ); }); it('should throw an error for range queries missing a value', () => { expect(() => { fromKueryExpression('foo > '); - }).toThrow('Expected literal, whitespace but end of input found.\n' + - 'foo > \n' + - '------^'); + }).toThrow('Expected literal, whitespace but end of input found.\n' + 'foo > \n' + '------^'); }); it('should throw an error for range queries missing a field', () => { expect(() => { fromKueryExpression('< 1000'); - }).toThrow('Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + - '< 1000\n' + - '^'); + }).toThrow( + 'Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + + '< 1000\n' + + '^' + ); }); - }); diff --git a/packages/kbn-es-query/src/kuery/errors/index.js b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts similarity index 55% rename from packages/kbn-es-query/src/kuery/errors/index.js rename to src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts index 82e1aee7b775a..7c90119fcc1bc 100644 --- a/packages/kbn-es-query/src/kuery/errors/index.js +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts @@ -20,35 +20,46 @@ import { repeat } from 'lodash'; import { i18n } from '@kbn/i18n'; -const endOfInputText = i18n.translate('kbnESQuery.kql.errors.endOfInputText', { +const endOfInputText = i18n.translate('data.common.esQuery.kql.errors.endOfInputText', { defaultMessage: 'end of input', }); -export class KQLSyntaxError extends Error { +const grammarRuleTranslations: Record = { + fieldName: i18n.translate('data.common.esQuery.kql.errors.fieldNameText', { + defaultMessage: 'field name', + }), + value: i18n.translate('data.common.esQuery.kql.errors.valueText', { + defaultMessage: 'value', + }), + literal: i18n.translate('data.common.esQuery.kql.errors.literalText', { + defaultMessage: 'literal', + }), + whitespace: i18n.translate('data.common.esQuery.kql.errors.whitespaceText', { + defaultMessage: 'whitespace', + }), +}; + +interface KQLSyntaxErrorData extends Error { + found: string; + expected: KQLSyntaxErrorExpected[]; + location: any; +} - constructor(error, expression) { - const grammarRuleTranslations = { - fieldName: i18n.translate('kbnESQuery.kql.errors.fieldNameText', { - defaultMessage: 'field name', - }), - value: i18n.translate('kbnESQuery.kql.errors.valueText', { - defaultMessage: 'value', - }), - literal: i18n.translate('kbnESQuery.kql.errors.literalText', { - defaultMessage: 'literal', - }), - whitespace: i18n.translate('kbnESQuery.kql.errors.whitespaceText', { - defaultMessage: 'whitespace', - }), - }; +interface KQLSyntaxErrorExpected { + description: string; +} + +export class KQLSyntaxError extends Error { + shortMessage: string; - const translatedExpectations = error.expected.map((expected) => { + constructor(error: KQLSyntaxErrorData, expression: any) { + const translatedExpectations = error.expected.map(expected => { return grammarRuleTranslations[expected.description] || expected.description; }); const translatedExpectationText = translatedExpectations.join(', '); - const message = i18n.translate('kbnESQuery.kql.errors.syntaxError', { + const message = i18n.translate('data.common.esQuery.kql.errors.syntaxError', { defaultMessage: 'Expected {expectedList} but {foundInput} found.', values: { expectedList: translatedExpectationText, @@ -56,11 +67,9 @@ export class KQLSyntaxError extends Error { }, }); - const fullMessage = [ - message, - expression, - repeat('-', error.location.start.offset) + '^', - ].join('\n'); + const fullMessage = [message, expression, repeat('-', error.location.start.offset) + '^'].join( + '\n' + ); super(fullMessage); this.name = 'KQLSyntaxError'; diff --git a/packages/kbn-es-query/src/kuery/node_types/function.js b/src/plugins/data/common/es_query/kuery/node_types/function.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/function.js rename to src/plugins/data/common/es_query/kuery/node_types/function.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.test.ts b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts new file mode 100644 index 0000000000000..ca9798eb6e74f --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { fields } from '../../../index_patterns/mocks'; + +import { nodeTypes } from './index'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function'; +// @ts-ignore +import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is'; + +describe('kuery node types', () => { + describe('function', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNode', () => { + test('should return a node representing the given kuery function', () => { + const result = buildNode('is', 'extension', 'jpg'); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + }); + }); + + describe('buildNodeWithArgumentNodes', () => { + test('should return a function node with the given argument list untouched', () => { + const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); + const valueLiteral = nodeTypes.literal.buildNode('jpg'); + const argumentNodes = [fieldNameLiteral, valueLiteral]; + const result = buildNodeWithArgumentNodes('is', argumentNodes); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + expect(result.arguments).toBe(argumentNodes); + expect(result.arguments).toEqual(argumentNodes); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given function type's ES query representation", () => { + const node = buildNode('is', 'extension', 'jpg'); + const expected = isFunctionToElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + + expect(expected).toEqual(result); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts similarity index 72% rename from packages/kbn-es-query/src/kuery/node_types/index.d.ts rename to src/plugins/data/common/es_query/kuery/node_types/index.d.ts index daf8032f9fe0e..720d64e11a0f8 100644 --- a/packages/kbn-es-query/src/kuery/node_types/index.d.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts @@ -21,7 +21,8 @@ * WARNING: these typings are incomplete */ -import { JsonObject, JsonValue } from '..'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode, JsonValue } from '..'; type FunctionName = | 'is' @@ -34,6 +35,17 @@ type FunctionName = | 'geoPolygon' | 'nested'; +interface FunctionType { + buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + toElasticsearchQuery: ( + node: any, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record + ) => JsonValue; +} + interface FunctionTypeBuildNode { type: 'function'; function: FunctionName; @@ -41,32 +53,40 @@ interface FunctionTypeBuildNode { arguments: any[]; } -interface FunctionType { - buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue; -} - interface LiteralType { - buildNode: ( - value: null | boolean | number | string - ) => { type: 'literal'; value: null | boolean | number | string }; + buildNode: (value: null | boolean | number | string) => LiteralTypeBuildNode; toElasticsearchQuery: (node: any) => null | boolean | number | string; } +interface LiteralTypeBuildNode { + type: 'literal'; + value: null | boolean | number | string; +} + interface NamedArgType { - buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any }; + buildNode: (name: string, value: any) => NamedArgTypeBuildNode; toElasticsearchQuery: (node: any) => string; } +interface NamedArgTypeBuildNode { + type: 'namedArg'; + name: string; + value: any; +} + interface WildcardType { - buildNode: (value: string) => { type: 'wildcard'; value: string }; + buildNode: (value: string) => WildcardTypeBuildNode; test: (node: any, string: string) => boolean; toElasticsearchQuery: (node: any) => string; toQueryStringQuery: (node: any) => string; hasLeadingWildcard: (node: any) => boolean; } +interface WildcardTypeBuildNode { + type: 'wildcard'; + value: string; +} + interface NodeTypes { function: FunctionType; literal: LiteralType; diff --git a/packages/kbn-es-query/src/kuery/node_types/index.js b/src/plugins/data/common/es_query/kuery/node_types/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/index.js rename to src/plugins/data/common/es_query/kuery/node_types/index.js diff --git a/packages/kbn-es-query/src/kuery/node_types/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.js diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.test.ts index 25fe2bcc45a45..60fe2d6d1013c 100644 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js +++ b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts @@ -17,34 +17,27 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as literal from '../literal'; +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './literal'; -describe('kuery node types', function () { +describe('kuery node types', () => { + describe('literal', () => { + describe('buildNode', () => { + test('should return a node representing the given value', () => { + const result = buildNode('foo'); - describe('literal', function () { - - describe('buildNode', function () { - - it('should return a node representing the given value', function () { - const result = literal.buildNode('foo'); - expect(result).to.have.property('type', 'literal'); - expect(result).to.have.property('value', 'foo'); + expect(result).toHaveProperty('type', 'literal'); + expect(result).toHaveProperty('value', 'foo'); }); - }); - describe('toElasticsearchQuery', function () { + describe('toElasticsearchQuery', () => { + test('should return the literal value represented by the given node', () => { + const node = buildNode('foo'); + const result = toElasticsearchQuery(node); - it('should return the literal value represented by the given node', function () { - const node = literal.buildNode('foo'); - const result = literal.toElasticsearchQuery(node); - expect(result).to.be('foo'); + expect(result).toBe('foo'); }); - }); - - }); - }); diff --git a/packages/kbn-es-query/src/kuery/node_types/named_arg.js b/src/plugins/data/common/es_query/kuery/node_types/named_arg.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/named_arg.js rename to src/plugins/data/common/es_query/kuery/node_types/named_arg.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts new file mode 100644 index 0000000000000..36c40d28e55c2 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from './index'; + +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './named_arg'; + +describe('kuery node types', () => { + describe('named arg', () => { + describe('buildNode', () => { + test('should return a node representing a named argument with the given value', () => { + const result = buildNode('fieldName', 'foo'); + expect(result).toHaveProperty('type', 'namedArg'); + expect(result).toHaveProperty('name', 'fieldName'); + expect(result).toHaveProperty('value'); + + const literalValue = result.value; + expect(literalValue).toHaveProperty('type', 'literal'); + expect(literalValue).toHaveProperty('value', 'foo'); + }); + + test('should support literal nodes as values', () => { + const value = nodeTypes.literal.buildNode('foo'); + const result = buildNode('fieldName', value); + + expect(result.value).toBe(value); + expect(result.value).toEqual(value); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the argument value represented by the given node', () => { + const node = buildNode('fieldName', 'foo'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo'); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/wildcard.js b/src/plugins/data/common/es_query/kuery/node_types/wildcard.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/wildcard.js rename to src/plugins/data/common/es_query/kuery/node_types/wildcard.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts new file mode 100644 index 0000000000000..7e221d96b49e9 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { + buildNode, + wildcardSymbol, + hasLeadingWildcard, + toElasticsearchQuery, + test as testNode, + toQueryStringQuery, + // @ts-ignore +} from './wildcard'; + +describe('kuery node types', () => { + describe('wildcard', () => { + describe('buildNode', () => { + test('should accept a string argument representing a wildcard string', () => { + const wildcardValue = `foo${wildcardSymbol}bar`; + const result = buildNode(wildcardValue); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result).toHaveProperty('value', wildcardValue); + }); + + test('should accept and parse a wildcard string', () => { + const result = buildNode('foo*bar'); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result.value).toBe(`foo${wildcardSymbol}bar`); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo*bar'); + }); + }); + + describe('toQueryStringQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('foo*bar'); + }); + + test('should escape query_string query special characters other than wildcard', () => { + const node = buildNode('+foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('\\+foo*bar'); + }); + }); + + describe('test', () => { + test('should return a boolean indicating whether the string matches the given wildcard node', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'foobazbar')).toBe(true); + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'fooqux')).toBe(false); + expect(testNode(node, 'bazbar')).toBe(false); + }); + + test('should return a true even when the string has newlines or tabs', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foo\nbar')).toBe(true); + expect(testNode(node, 'foo\tbar')).toBe(true); + }); + }); + + describe('hasLeadingWildcard', () => { + test('should determine whether a wildcard node contains a leading wildcard', () => { + const node = buildNode('foo*bar'); + expect(hasLeadingWildcard(node)).toBe(false); + + const leadingWildcardNode = buildNode('*foobar'); + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(true); + }); + + // Lone wildcards become exists queries, so we aren't worried about their performance + test('should not consider a lone wildcard to be a leading wildcard', () => { + const leadingWildcardNode = buildNode('*'); + + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(false); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/src/plugins/data/common/es_query/kuery/types.ts similarity index 73% rename from packages/kbn-es-query/src/kuery/index.d.ts rename to src/plugins/data/common/es_query/kuery/types.ts index b01a8914f68ef..86cb7e08a767c 100644 --- a/packages/kbn-es-query/src/kuery/index.d.ts +++ b/src/plugins/data/common/es_query/kuery/types.ts @@ -17,14 +17,29 @@ * under the License. */ -export * from './ast'; +import { NodeTypes } from './node_types'; + +export interface KueryNode { + type: keyof NodeTypes; + [key: string]: any; +} + +export type DslQuery = any; + +export interface KueryParseOptions { + helpers: { + [key: string]: any; + }; + startRule: string; + allowLeadingWildcards: boolean; + errorOnLuceneSyntax: boolean; +} + export { nodeTypes } from './node_types'; +export type JsonArray = JsonValue[]; export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; export interface JsonObject { [key: string]: JsonValue; } - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index 687870306c873..1c17e231cace8 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -17,10 +17,10 @@ * under the License. */ -import { FieldFormat } from '../field_format'; +import { FieldFormat, IFieldFormatType } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert) => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 6b5f665c6e20e..dd445a33f21c5 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -73,7 +73,7 @@ export abstract class FieldFormat { */ public type: any = this.constructor; - private readonly _params: any; + protected readonly _params: any; protected getConfig: Function | undefined; constructor(_params: any = {}, getConfig?: Function) { diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 7c3e597ae12d2..a9d066f3cd49f 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -19,8 +19,8 @@ module.exports = { kuery: { - src: 'packages/kbn-es-query/src/kuery/ast/kuery.peg', - dest: 'packages/kbn-es-query/src/kuery/ast/kuery.js', + src: 'src/plugins/data/common/es_query/kuery/ast/kuery.peg', + dest: 'src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js', options: { allowedStartRules: ['start', 'Literal'] } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 52be4d4fba774..32fbe46ac560c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; // @ts-ignore @@ -16,13 +15,14 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; +import { usePlugins } from '../../../new-platform/plugin'; +import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { - AutocompleteSuggestion, AutocompleteProvider, + AutocompleteSuggestion, + esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { usePlugins } from '../../../new-platform/plugin'; const Container = styled.div` margin-bottom: 10px; @@ -34,8 +34,8 @@ interface State { } function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index cee097d010212..a6f6d36ecfc81 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query'; import { ESFilter } from '../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui-filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; @@ -12,10 +11,13 @@ import { localUIFilters, localUIFilterNames } from '../../ui_filters/local_ui_filters/config'; -import { StaticIndexPattern } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { + esKuery, + IIndexPattern +} from '../../../../../../../../src/plugins/data/server'; export function getUiFiltersES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFilters: UIFilters ) { const { kuery, environment, ...localFilterValues } = uiFilters; @@ -43,13 +45,13 @@ export function getUiFiltersES( } function getKueryUiFilterES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, kuery?: string ) { if (!kuery || !indexPattern) { return; } - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern) as ESFilter; + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern) as ESFilter; } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 526728bd77cac..83c610800b89b 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; import { setup as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; const getAutocompleteProvider = (language: string) => @@ -20,7 +19,7 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { public isKueryValid(kuery: string): boolean { try { - fromKueryExpression(kuery); + esKuery.fromKueryExpression(kuery); } catch (err) { return false; } @@ -31,9 +30,9 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { if (!this.isKueryValid(kuery)) { return ''; } - const ast = fromKueryExpression(kuery); + const ast = esKuery.fromKueryExpression(kuery); const indexPattern = await this.getIndexPattern(); - return JSON.stringify(toElasticsearchQuery(ast, indexPattern)); + return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); } public async getSuggestions( kuery: string, diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index 82e50c702997f..56458e5de273f 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -9,12 +9,9 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { IDataPluginServices, Query } from 'src/plugins/data/public'; import { IndexPatternSavedObject, IndexPatternProvider } from '../types'; import { QueryBarInput, IndexPattern } from '../../../../../../src/legacy/core_plugins/data/public'; import { openSourceModal } from '../services/source_modal'; - import { GraphState, datasourceSelector, @@ -23,6 +20,7 @@ import { } from '../state_management'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { IDataPluginServices, Query, esKuery } from '../../../../../../src/plugins/data/public'; export interface OuterSearchBarProps { isLoading: boolean; @@ -44,7 +42,10 @@ export interface SearchBarProps extends OuterSearchBarProps { function queryToString(query: Query, indexPattern: IndexPattern) { if (query.language === 'kuery' && typeof query.query === 'string') { - const dsl = toElasticsearchQuery(fromKueryExpression(query.query as string), indexPattern); + const dsl = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ); // JSON representation of query will be handled by existing logic. // TODO clean this up and handle it in the data fetch layer once // it moved to typescript. diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx index 1353b065bc444..a851f8380b915 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; @@ -12,6 +11,7 @@ import { StaticIndexPattern } from 'ui/index_patterns'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../autocomplete_field'; import { isDisplayable } from '../../utils/is_displayable'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: StaticIndexPattern; @@ -21,7 +21,7 @@ interface Props { function validateQuery(query: string) { try { - fromKueryExpression(query); + esKuery.fromKueryExpression(query); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts index 069f631b9c026..f17f7be4defe9 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts @@ -5,10 +5,8 @@ */ import { createSelector } from 'reselect'; - -import { fromKueryExpression } from '@kbn/es-query'; - import { LogFilterState } from './reducer'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; export const selectLogFilterQuery = (state: LogFilterState) => state.filterQuery ? state.filterQuery.query : null; @@ -23,7 +21,7 @@ export const selectIsLogFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts index 7d518b5e20f2d..0acce82950f77 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts @@ -6,8 +6,7 @@ import { createSelector } from 'reselect'; -import { fromKueryExpression } from '@kbn/es-query'; - +import { esKuery } from '../../../../../../../../src/plugins/data/public'; import { WaffleFilterState } from './reducer'; export const selectWaffleFilterQuery = (state: WaffleFilterState) => @@ -23,7 +22,7 @@ export const selectIsWaffleFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/utils/kuery.ts b/x-pack/legacy/plugins/infra/public/utils/kuery.ts index 4a767f2777512..2e793d53b4622 100644 --- a/x-pack/legacy/plugins/infra/public/utils/kuery.ts +++ b/x-pack/legacy/plugins/infra/public/utils/kuery.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esKuery } from '../../../../../../src/plugins/data/public'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, @@ -13,7 +13,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js index 8a82194470ace..7b0e42283d5f5 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js @@ -5,11 +5,11 @@ */ import { flatten, mapValues, uniq } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { getSuggestionsProvider as field } from './field'; import { getSuggestionsProvider as value } from './value'; import { getSuggestionsProvider as operator } from './operator'; import { getSuggestionsProvider as conjunction } from './conjunction'; +import { esKuery } from '../../../../../../src/plugins/data/public'; const cursorSymbol = '@kuery-cursor@'; @@ -27,7 +27,7 @@ export const kueryProvider = ({ config, indexPatterns, boolFilter }) => { let cursorNode; try { - cursorNode = fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); + cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); } catch (e) { cursorNode = {}; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js index 16e4a563c33ae..18b3382175fdd 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { npStart } from 'ui/new_platform'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); @@ -35,8 +35,8 @@ export async function getSuggestions( } function convertKueryToEsQuery(kuery, indexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } // Recommended by MDN for escaping user input to be treated as a literal string within a regular expression // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions @@ -53,7 +53,7 @@ export function escapeDoubleQuotes(string) { } export function getKqlQueryValues(inputValue, indexPattern) { - const ast = fromKueryExpression(inputValue); + const ast = esKuery.fromKueryExpression(inputValue); const isAndOperator = (ast.function === 'and'); const query = convertKueryToEsQuery(inputValue, indexPattern); const filteredFields = []; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index b6819e54575d6..d82079dd05d31 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query'; import { isEmpty, isString, flow } from 'lodash/fp'; import { Query, esFilters, esQuery, + esKuery, IIndexPattern, } from '../../../../../../../src/plugins/data/public'; @@ -21,7 +21,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; @@ -31,10 +33,10 @@ export const convertKueryToElasticSearchQuery = ( export const convertKueryToDslFilter = ( kueryExpression: string, indexPattern: IIndexPattern -): JsonObject => { +): esKuery.JsonObject => { try { return kueryExpression - ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) : {}; } catch (err) { return {}; @@ -55,7 +57,7 @@ export const escapeQueryValue = (val: number | string = ''): string | number => export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { try { - fromKueryExpression(kqlFilterQuery.expression); + esKuery.fromKueryExpression(kqlFilterQuery.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx index 500557a2c2a96..58a9e57b32ce6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; -import { fromKueryExpression } from '@kbn/es-query'; import { isEmpty } from 'lodash/fp'; import React from 'react'; @@ -17,6 +16,7 @@ import { } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; import * as CreateRuleI18n from '../../translations'; @@ -106,7 +106,7 @@ export const schema: FormSchema = { const { query } = value as FieldValueQueryBar; if (!isEmpty(query.query as string) && query.language === 'kuery') { try { - fromKueryExpression(query.query); + esKuery.fromKueryExpression(query.query); } catch (err) { return { code: 'ERR_FIELD_FORMAT', diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index 16100089a9e56..b465392a50ae1 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -104,7 +104,7 @@ export function createSearchItems( const filters = fs.length ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(config); - combinedQuery = esQuery.buildEsQuery(indexPattern || null, [query], filters, esQueryConfigs); + combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); } return { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index f529c9cd9d53f..da392660eb70e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -9,14 +9,17 @@ import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { AutocompleteProviderRegister, AutocompleteSuggestion } from 'src/plugins/data/public'; -import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public/index_patterns/index_patterns'; import { Typeahead } from './typeahead'; import { getIndexPattern } from '../../../lib/adapters/index_pattern'; import { UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; +import { + AutocompleteProviderRegister, + AutocompleteSuggestion, + esKuery, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; @@ -27,15 +30,15 @@ interface State { isLoadingIndexPattern: boolean; } -function convertKueryToEsQuery(kuery: string, indexPattern: unknown) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( query: string, selectionStart: number, - apmIndexPattern: StaticIndexPattern, + apmIndexPattern: IIndexPattern, autocomplete: Pick ) { const autocompleteProvider = autocomplete.getProvider('kuery'); diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ded16c3f8eb2f..09d40d32b696c 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -6,10 +6,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { getOverviewPageBreadcrumbs } from '../breadcrumbs'; import { EmptyState, @@ -26,6 +24,7 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { getIndexPattern } from '../lib/adapters/index_pattern'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; +import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; interface OverviewPageProps { basePath: string; @@ -109,8 +108,8 @@ export const OverviewPage = ({ if (indexPattern) { const staticIndexPattern = toStaticIndexPattern(indexPattern); const combinedFilterString = combineFiltersAndUserSearch(filterQueryString, kueryString); - const ast = fromKueryExpression(combinedFilterString); - const elasticsearchQuery = toElasticsearchQuery(ast, staticIndexPattern); + const ast = esKuery.fromKueryExpression(combinedFilterString); + const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, staticIndexPattern); filters = JSON.stringify(elasticsearchQuery); } } diff --git a/x-pack/package.json b/x-pack/package.json index eccc5918e6d50..d97fd38676bde 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -185,7 +185,6 @@ "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/elastic-idx": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f5fc453557122..217b20797492a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2527,12 +2527,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "フィールド詳細を切り替える", - "kbnESQuery.kql.errors.endOfInputText": "インプットの終わり", - "kbnESQuery.kql.errors.fieldNameText": "フィールド名", - "kbnESQuery.kql.errors.literalText": "文字通り", - "kbnESQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", - "kbnESQuery.kql.errors.valueText": "値", - "kbnESQuery.kql.errors.whitespaceText": "ホワイトスペース", + "data.common.esQuery.kql.errors.endOfInputText": "インプットの終わり", + "data.common.esQuery.kql.errors.fieldNameText": "フィールド名", + "data.common.esQuery.kql.errors.literalText": "文字通り", + "data.common.esQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", + "data.common.esQuery.kql.errors.valueText": "値", + "data.common.esQuery.kql.errors.whitespaceText": "ホワイトスペース", "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", "kbnVislibVisTypes.area.areaTitle": "エリア", "kbnVislibVisTypes.area.groupTitle": "系列を分割", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 288fc92be3cbd..6a2ba20af7714 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2528,12 +2528,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "切换字段详细信息", - "kbnESQuery.kql.errors.endOfInputText": "输入结束", - "kbnESQuery.kql.errors.fieldNameText": "字段名称", - "kbnESQuery.kql.errors.literalText": "文本", - "kbnESQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", - "kbnESQuery.kql.errors.valueText": "值", - "kbnESQuery.kql.errors.whitespaceText": "空白", + "data.common.esQuery.kql.errors.endOfInputText": "输入结束", + "data.common.esQuery.kql.errors.fieldNameText": "字段名称", + "data.common.esQuery.kql.errors.literalText": "文本", + "data.common.esQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", + "data.common.esQuery.kql.errors.valueText": "值", + "data.common.esQuery.kql.errors.whitespaceText": "空白", "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", "kbnVislibVisTypes.area.areaTitle": "面积图", "kbnVislibVisTypes.area.groupTitle": "拆分序列", From a234e8b836007754240b4e215b865df1ad0e5fee Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 26 Nov 2019 12:36:35 -0800 Subject: [PATCH 57/63] [DOCS] Fixes broken links (#51634) --- docs/api/role-management/put.asciidoc | 4 ++- docs/developer/security/rbac.asciidoc | 9 ++++++- .../tutorial-full-experience.asciidoc | 2 +- .../tutorial-sample-data.asciidoc | 4 +-- docs/management/managing-indices.asciidoc | 2 +- docs/setup/install.asciidoc | 4 +-- .../security/authentication/index.asciidoc | 25 ++++++++++--------- docs/user/security/reporting.asciidoc | 8 +++--- docs/user/security/securing-kibana.asciidoc | 2 +- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index 67ec15892afe4..a00fedf7e7ac4 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -26,7 +26,9 @@ To use the create or update role API, you must have the `manage_security` cluste (Optional, object) In the `metadata` object, keys that begin with `_` are reserved for system usage. `elasticsearch`:: - (Optional, object) {es} cluster and index privileges. Valid keys include `cluster`, `indices`, and `run_as`. For more information, see {xpack-ref}/defining-roles.html[Defining Roles]. + (Optional, object) {es} cluster and index privileges. Valid keys include + `cluster`, `indices`, and `run_as`. For more information, see + {ref}/defining-roles.html[Defining roles]. `kibana`:: (list) Objects that specify the <> for the role: diff --git a/docs/developer/security/rbac.asciidoc b/docs/developer/security/rbac.asciidoc index b967dabf0684f..02b8233a9a3df 100644 --- a/docs/developer/security/rbac.asciidoc +++ b/docs/developer/security/rbac.asciidoc @@ -1,7 +1,14 @@ [[development-security-rbac]] === Role-based access control -Role-based access control (RBAC) in {kib} relies upon the {xpack-ref}/security-privileges.html#application-privileges[application privileges] that Elasticsearch exposes. This allows {kib} to define the privileges that {kib} wishes to grant to users, assign them to the relevant users using roles, and then authorize the user to perform a specific action. This is handled within a secured instance of the `SavedObjectsClient` and available transparently to consumers when using `request.getSavedObjectsClient()` or `savedObjects.getScopedSavedObjectsClient()`. +Role-based access control (RBAC) in {kib} relies upon the +{ref}/security-privileges.html#application-privileges[application privileges] +that Elasticsearch exposes. This allows {kib} to define the privileges that +{kib} wishes to grant to users, assign them to the relevant users using roles, +and then authorize the user to perform a specific action. This is handled within +a secured instance of the `SavedObjectsClient` and available transparently to +consumers when using `request.getSavedObjectsClient()` or +`savedObjects.getScopedSavedObjectsClient()`. [[development-rbac-privileges]] ==== {kib} Privileges diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index eafbb7d8f7c91..a05205fceab4a 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -91,7 +91,7 @@ and whether it's _tokenized_, or broken up into separate words. NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {xpack-ref}/security-privileges.html[Security Privileges] +index privileges. See {ref}/security-privileges.html[Security privileges] for more information. In Kibana *Dev Tools > Console*, set up a mapping for the Shakespeare data set: diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index 24cc176d5daf9..f41c648a3d492 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -12,8 +12,8 @@ with Kibana sample data and learn to: NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See {xpack-ref}/security-privileges.html[Security Privileges] -for more information. +on the `kibana_sample_data_*` indices. See +{ref}/security-privileges.html[Security privileges] for more information. [float] diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index 4a736e3ddab59..4c7f6c2aee6e6 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -22,7 +22,7 @@ If security is enabled, you must have the `monitor` cluster privilege and the `view_index_metadata` and `manage` index privileges to view the data. For index templates, you must have the `manage_index_templates` cluster privilege. -See {xpack-ref}/security-privileges.html[Security Privileges] for more +See {ref}/security-privileges.html[Security privileges] for more information. Before using this feature, you should be familiar with index management diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index b0893a6e78945..286fed34f64c5 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -54,8 +54,8 @@ Formulae are available from the Elastic Homebrew tap for installing {kib} on mac <> IMPORTANT: If your Elasticsearch installation is protected by -{xpack-ref}/elasticsearch-security.html[{security}] see -{kibana-ref}/using-kibana-with-security.html[Configuring Security in Kibana] for +{ref}/elasticsearch-security.html[{security}] see +{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana] for additional setup instructions. include::install/targz.asciidoc[] diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index e6b70fa059fc2..32f341a9c1b7c 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -14,16 +14,17 @@ - <> [[basic-authentication]] -==== Basic Authentication +==== Basic authentication Basic authentication requires a username and password to successfully log in to {kib}. It is enabled by default and based on the Native security realm provided by {es}. The basic authentication provider uses a Kibana provided login form, and supports authentication using the `Authorization` request header's `Basic` scheme. The session cookies that are issued by the basic authentication provider are stateless. Therefore, logging out of Kibana when using the basic authentication provider clears the session cookies from the browser but does not invalidate the session cookie for reuse. -For more information about basic authentication and built-in users, see {xpack-ref}/setting-up-authentication.html[Setting Up User Authentication]. +For more information about basic authentication and built-in users, see +{ref}/setting-up-authentication.html[User authentication]. [[token-authentication]] -==== Token Authentication +==== Token authentication Token authentication allows users to login using the same Kibana provided login form as basic authentication. The token authentication provider is built on {es}'s token APIs. The bearer tokens returned by {es}'s {ref}/security-api-get-token.html[get token API] can be used directly with Kibana using the `Authorization` request header with the `Bearer` scheme. @@ -46,7 +47,7 @@ xpack.security.authc.providers: [token, basic] -------------------------------------------------------------------------------- [[pki-authentication]] -==== Public Key Infrastructure (PKI) Authentication +==== Public key infrastructure (PKI) authentication [IMPORTANT] ============================================================================ @@ -76,9 +77,9 @@ xpack.security.authc.providers: [pki, basic] Note that with `server.ssl.clientAuthentication` set to `required`, users are asked to provide a valid client certificate, even if they want to authenticate with username and password. Depending on the security policies, it may or may not be desired. If not, `server.ssl.clientAuthentication` can be set to `optional`. In this case, {kib} still requests a client certificate, but the client won't be required to present one. The `optional` client authentication mode might also be needed in other cases, for example, when PKI authentication is used in conjunction with Reporting. [[saml]] -==== SAML Single Sign-On +==== SAML single sign-on -SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {xpack-ref}/saml-guide.html[Configuring SAML Single-Sign-On on the Elastic Stack]. +SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {ref}/saml-guide.html[Configuring SAML single sign-on on the Elastic Stack]. Set the configuration values in `kibana.yml` as follows: @@ -106,7 +107,7 @@ server.xsrf.whitelist: [/api/security/saml/callback] Users will be able to log in to {kib} via SAML Single Sign-On by navigating directly to the {kib} URL. Users who aren't authenticated are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session—users who logged in to a different application using the same Identity Provider in the same browser are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force user to re-authenticate. This login scenario is called _Service Provider initiated login_. [float] -===== SAML and Basic Authentication +===== SAML and basic authentication SAML support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both SAML and Basic authentication for the same {kib} instance: @@ -135,7 +136,7 @@ xpack.security.authc.saml.maxRedirectURLSize: 1kb -------------------------------------------------------------------------------- [[oidc]] -==== OpenID Connect Single Sign-On +==== OpenID Connect single sign-on Similar to SAML, authentication with OpenID Connect allows users to log in to {kib} using an OpenID Connect Provider such as Google, or Okta. OpenID Connect should also be configured in {es}. For more details, see {ref}/oidc-guide.html[Configuring single sign-on to the {stack} using OpenID Connect]. @@ -166,7 +167,7 @@ server.xsrf.whitelist: [/api/security/v1/oidc] -------------------------------------------------------------------------------- [float] -===== OpenID Connect and Basic Authentication +===== OpenID Connect and basic authentication Similar to SAML, OpenID Connect support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both OpenID Connect and Basic authentication for the same {kib} instance: @@ -179,12 +180,12 @@ xpack.security.authc.providers: [oidc, basic] Users will be able to access the login page and use Basic authentication by navigating to the `/login` URL. [float] -==== Single Sign-On provider details +==== Single sign-on provider details The following sections apply both to <> and <> [float] -===== Access and Refresh Tokens +===== Access and refresh tokens Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider @@ -202,7 +203,7 @@ If {kib} can't redirect the user to the external authentication provider (for ex indicates that both access and refresh tokens are expired. Reloading the current {kib} page fixes the error. [float] -===== Local and Global Logout +===== Local and global logout During logout, both the {kib} session cookie and access/refresh token pair are invalidated. Even if the cookie has been leaked, it can't be re-used after logout. This is known as "local" logout. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index fb40dc17c0abd..aaba60ca4b3ca 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -8,7 +8,7 @@ user actions in {kib}. To use {reporting} with {security} enabled, you need to <>. If you are automatically generating reports with -{xpack-ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} +{ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} to trust the {kib} server's certificate. For more information, see <>. @@ -35,7 +35,7 @@ POST /_security/user/reporter * If you are using an LDAP or Active Directory realm, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in -{xpack-ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. +{ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_user` and `reporting_user` roles: + @@ -55,7 +55,7 @@ In a production environment, you should restrict access to the {reporting} endpoints to authorized users. This requires that you: . Enable {security} on your {es} cluster. For more information, -see {xpack-ref}/security-getting-started.html[Getting Started with Security]. +see {ref}/security-getting-started.html[Getting Started with Security]. . Configure an SSL certificate for Kibana. For more information, see <>. . Configure {watcher} to trust the Kibana server's certificate by adding it to @@ -83,4 +83,4 @@ includes a watch that submits requests as the built-in `elastic` user: <>. For more information about configuring watches, see -{xpack-ref}/how-watcher-works.html[How Watcher Works]. +{ref}/how-watcher-works.html[How Watcher works]. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 2fbc6ba4f1ee6..60f5473f43b9d 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -121,7 +121,7 @@ TIP: You can define as many different roles for your {kib} users as you need. For example, create roles that have `read` and `view_index_metadata` privileges on specific index patterns. For more information, see -{xpack-ref}/authorization.html[Configuring Role-based Access Control]. +{ref}/authorization.html[User authorization]. -- From 9d8c93158c2532e636f49c9cb13b92aedefc90d3 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 26 Nov 2019 16:19:13 -0700 Subject: [PATCH 58/63] [SIEM][Detection Engine] Adds signal to ECS event.kind and fixes status in signals (#51772) ## Summary * Adds signal to the ECS event.kind when it copies over a signal * Creates a `original_event` if needed within signal so additional look ups don't have to happen * Fixes a bug with `signal.status` where it was not plumbed correctly * Adds extra unit tests around the filter * Adds missing unit tests around utils I didn't add before * Fixes a typing issue with output of a signal Example signal output: Original event turns into this: ```ts "event" : { "dataset" : "socket", "kind" : "signal", "action" : "existing_socket", "id" : "ffec6797-b92f-4436-bb40-69bac2c21874", "module" : "system" }, ``` Signal amplification turns into this where it contains original_event looks like this: ```ts "signal" : { "parent" : { "id" : "xNRlqW4BHe9nqdOi2358", "type" : "event", "index" : "auditbeat", "depth" : 1 }, "original_time" : "2019-11-26T20:27:11.169Z", "status" : "open", "rule" : { "id" : "643fbd2f-a3c9-449e-ba95-e3d89000a72a", "rule_id" : "rule-1", "false_positives" : [ ], "max_signals" : 100, "risk_score" : 1, "description" : "Detecting root and admin users", "from" : "now-6m", "immutable" : false, "interval" : "5m", "language" : "kuery", "name" : "Detect Root/Admin Users", "query" : "user.name: root or user.name: admin", "references" : [ "http://www.example.com", "https://ww.example.com" ], "severity" : "high", "tags" : [ ], "type" : "query", "to" : "now", "enabled" : true, "created_by" : "elastic_some_user", "updated_by" : "elastic_some_user" }, "original_event" : { "dataset" : "socket", "kind" : "state", "action" : "existing_socket", "id" : "ffec6797-b92f-4436-bb40-69bac2c21874", "module" : "system" } } ``` ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../alerts/__mocks__/es_results.ts | 57 +- .../alerts/get_filter.test.ts | 98 ++++ .../lib/detection_engine/alerts/types.ts | 4 - .../lib/detection_engine/alerts/utils.test.ts | 536 +++++++++++++++++- .../lib/detection_engine/alerts/utils.ts | 26 +- .../lib/detection_engine/signals_mapping.json | 72 +++ .../legacy/plugins/siem/server/lib/types.ts | 7 +- 7 files changed, 750 insertions(+), 50 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 079d3658461fa..8080bd5ddd913 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, RuleTypeParams } from '../types'; +import { + SignalSourceHit, + SignalSearchResponse, + RuleTypeParams, + OutputRuleAlertRest, +} from '../types'; export const sampleRuleAlertParams = ( - maxSignals: number | undefined, + maxSignals?: number | undefined, riskScore?: number | undefined ): RuleTypeParams => ({ ruleId: 'rule-1', @@ -32,7 +37,7 @@ export const sampleRuleAlertParams = ( meta: undefined, }); -export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ +export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -44,7 +49,7 @@ export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ }, }); -export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => ({ +export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -55,7 +60,7 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => }, }); -export const sampleDocWithSortId = (someUuid: string): SignalSourceHit => ({ +export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -138,7 +143,9 @@ export const sampleBulkCreateDuplicateResult = { ], }; -export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchResponse => ({ +export const sampleDocSearchResultsNoSortId = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -159,7 +166,7 @@ export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchRe }); export const sampleDocSearchResultsNoSortIdNoVersion = ( - someUuid: string + someUuid: string = sampleIdGuid ): SignalSearchResponse => ({ took: 10, timed_out: false, @@ -180,7 +187,9 @@ export const sampleDocSearchResultsNoSortIdNoVersion = ( }, }); -export const sampleDocSearchResultsNoSortIdNoHits = (someUuid: string): SignalSearchResponse => ({ +export const sampleDocSearchResultsNoSortIdNoHits = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -222,7 +231,9 @@ export const repeatedSearchResultsWithSortId = ( }, }); -export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearchResponse => ({ +export const sampleDocSearchResultsWithSortId = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -243,3 +254,31 @@ export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearch }); export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; + +export const sampleRule = (): Partial => { + return { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts index 9f72da44e963b..c55c99fb291c4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { AlertServices } from '../../../../../alerting/server/types'; +import { PartialFilter } from './types'; describe('get_filter', () => { let savedObjectsClient = savedObjectsClientMock.create(); @@ -145,6 +146,103 @@ describe('get_filter', () => { }); }); + test('it should work with a simple filter as a kuery without meta information', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery without meta information with an exists', () => { + const query: PartialFilter = { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + } as PartialFilter; + + const exists: PartialFilter = { + exists: { + field: 'host.hostname', + }, + } as PartialFilter; + + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [query, exists], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + { + exists: { + field: 'host.hostname', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + test('it should work with a simple filter that is disabled as a kuery', () => { const esQuery = getQueryFilter( 'host.name: windows', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 28431b8165266..462a9b7d65ee2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -65,10 +65,6 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { updated_by: string | undefined | null; }; -export type OutputRuleES = OutputRuleAlertRest & { - status: 'open' | 'closed'; -}; - export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; rule_id: RuleAlertParams['ruleId'] | undefined; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index 19c8d5ccc87ca..fc50e54e06e4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -14,6 +14,9 @@ import { singleBulkCreate, singleSearchAfter, searchAfterAndBulkCreate, + buildEventTypeSignal, + buildSignal, + buildRule, } from './utils'; import { sampleDocNoSortId, @@ -26,8 +29,12 @@ import { repeatedSearchResultsWithSortId, sampleBulkCreateDuplicateResult, sampleRuleGuid, + sampleRule, + sampleIdGuid, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { OutputRuleAlertRest } from './types'; +import { Signal } from '../../types'; const mockLogger: Logger = { log: jest.fn(), @@ -51,10 +58,9 @@ describe('utils', () => { }); describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId(fakeUuid), + doc: sampleDocNoSortId(), ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', @@ -65,18 +71,225 @@ describe('utils', () => { }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; - if (fakeSignalSourceHit.signal.parent) { - delete fakeSignalSourceHit.signal.parent.id; - } expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', + event: { + kind: 'signal', + }, signal: { parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with but no kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + module: 'system', + }, + parent: { + id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', depth: 1, }, original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with with only kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + kind: 'signal', + }, + signal: { + original_event: { + kind: 'event', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', rule: { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', @@ -96,7 +309,6 @@ describe('utils', () => { severity: 'high', tags: ['some fake tag'], type: 'query', - status: 'open', to: 'now', enabled: true, created_by: 'elastic', @@ -213,8 +425,7 @@ describe('utils', () => { }); }); test('create successful bulk create', async () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -226,7 +437,7 @@ describe('utils', () => { ], }); const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(fakeUuid), + someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -241,8 +452,7 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create successful bulk create with docs with no versioning', async () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -254,7 +464,7 @@ describe('utils', () => { ], }); const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(fakeUuid), + someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -269,7 +479,7 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create unsuccessful bulk create due to empty search results', async () => { - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); const successfulsingleBulkCreate = await singleBulkCreate({ @@ -288,12 +498,11 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create successful bulk create when bulk create has errors', async () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(fakeUuid), + someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -312,7 +521,7 @@ describe('utils', () => { describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ @@ -326,7 +535,7 @@ describe('utils', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, @@ -339,7 +548,7 @@ describe('utils', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); @@ -356,7 +565,7 @@ describe('utils', () => { }); describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, ruleParams: sampleParams, @@ -446,8 +655,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleRuleAlertParams(undefined); - const someUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -458,7 +666,7 @@ describe('utils', () => { ], }); const result = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortId(someUuid), + someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -475,8 +683,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleRuleAlertParams(undefined); - const someUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -487,7 +694,7 @@ describe('utils', () => { ], }); const result = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), + someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -504,7 +711,6 @@ describe('utils', () => { }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { const sampleParams = sampleRuleAlertParams(10); - const oneGuid = uuid.v4(); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -516,7 +722,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); + .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, @@ -596,4 +802,276 @@ describe('utils', () => { expect(result).toEqual(false); }); }); + + describe('buildEventTypeSignal', () => { + test('it returns the event appended of kind signal if it does not exist', () => { + const doc = sampleDocNoSortId(); + delete doc._source.event; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event appended of kind signal if it is an empty object', () => { + const doc = sampleDocNoSortId(); + doc._source.event = {}; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event with kind signal and other properties if they exist', () => { + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const eventType = buildEventTypeSignal(doc); + const expected: object = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'signal', + }; + expect(eventType).toEqual(expected); + }); + }); + + describe('buildSignal', () => { + test('it builds a signal as expected without original_event if event does not exist', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + delete doc._source.event; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + + test('it builds a signal as expected with original_event if is present', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + doc._source.event = { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + }); + + describe('buildRule', () => { + test('it builds a rule as expected with filters present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ]; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: false, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + filters: [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ], + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if enabled is null if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if filters is undefined if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index ba3f310c886ce..c3988b8fea458 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -14,7 +14,7 @@ import { SignalSearchResponse, BulkResponse, RuleTypeParams, - OutputRuleES, + OutputRuleAlertRest, } from './types'; import { buildEventsSearchQuery } from './build_events_query'; @@ -36,10 +36,9 @@ export const buildRule = ({ createdBy, updatedBy, interval, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, - status: 'open', rule_id: ruleParams.ruleId, false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, @@ -68,8 +67,8 @@ export const buildRule = ({ }); }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { - return { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { + const signal: Signal = { parent: { id: doc._id, type: 'event', @@ -77,8 +76,13 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial): depth: 1, }, original_time: doc._source['@timestamp'], + status: 'open', rule, }; + if (doc._source.event != null) { + return { ...signal, original_event: doc._source.event }; + } + return signal; }; interface BuildBulkBodyParams { @@ -92,6 +96,14 @@ interface BuildBulkBodyParams { enabled: boolean; } +export const buildEventTypeSignal = (doc: SignalSourceHit): object => { + if (doc._source.event != null && doc._source.event instanceof Object) { + return { ...doc._source.event, kind: 'signal' }; + } else { + return { kind: 'signal' }; + } +}; + // format search_after result for signals index. export const buildBulkBody = ({ doc, @@ -113,9 +125,11 @@ export const buildBulkBody = ({ interval, }); const signal = buildSignal(doc, rule); + const event = buildEventTypeSignal(doc); const signalHit: SignalHit = { ...doc._source, '@timestamp': new Date().toISOString(), + event, signal, }; return signalHit; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json index a95c9625a0003..dfe3caed5b71a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json @@ -107,6 +107,78 @@ }, "original_time": { "type": "date" + }, + "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" + } + } + }, + "status": { + "type": "keyword" } } }, diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9c0059d0d109d..c53805dc95fe7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,7 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { RuleAlertParamsRest } from './detection_engine/alerts/types'; +import { SearchTypes, OutputRuleAlertRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -66,7 +66,7 @@ export interface SiemContext { } export interface Signal { - rule: Partial; + rule: Partial; parent: { id: string; type: string; @@ -74,10 +74,13 @@ export interface Signal { depth: number; }; original_time: string; + original_event?: SearchTypes; + status: 'open' | 'closed'; } export interface SignalHit { '@timestamp': string; + event: object; signal: Partial; } From 0039e97747dffda32d3bad62986c72691cb24699 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 27 Nov 2019 01:55:48 +0200 Subject: [PATCH 59/63] [Telemetry] collector set to np (#51618) * first iteration * local collection ready * type check * fix collectorSet tests * unskip test * ordering * collectors as array in constructor * update README files * update README and canvas to check for optional dep * update README with more details * Add file path for README example * type UsageCollectionSetup * run type check after refactor --- .github/CODEOWNERS | 1 + src/core/CONVENTIONS.md | 37 ++++ src/legacy/core_plugins/kibana/index.js | 5 +- .../lib/csp_usage_collector/csp_collector.ts | 8 +- .../make_kql_usage_collector.js | 6 +- .../make_kql_usage_collector.test.js | 15 +- src/legacy/core_plugins/telemetry/README.md | 9 + src/legacy/core_plugins/telemetry/index.ts | 21 +- .../telemetry/server/collection_manager.ts | 5 +- .../telemetry/server/collectors/index.ts | 8 +- .../server/collectors/localization/index.ts | 2 +- .../telemetry_localization_collector.ts | 15 +- .../collectors/telemetry_plugin/index.ts | 2 +- .../telemetry_plugin_collector.ts | 16 +- .../server/collectors/ui_metric/index.ts | 2 +- .../telemetry_ui_metric_collector.ts | 8 +- .../server/collectors/usage/index.ts | 2 +- .../usage/telemetry_usage_collector.test.ts | 22 +- .../usage/telemetry_usage_collector.ts | 35 ++- .../core_plugins/telemetry/server/index.ts | 2 +- .../core_plugins/telemetry/server/plugin.ts | 20 +- .../__tests__/get_local_stats.js | 20 +- .../server/telemetry_collection/get_kibana.js | 11 +- .../telemetry_collection/get_local_stats.ts | 5 +- src/legacy/server/kbn_server.d.ts | 10 +- src/legacy/server/kbn_server.js | 2 - .../server/sample_data/usage/collector.ts | 25 +-- .../collectors/get_ops_stats_collector.js | 12 +- src/legacy/server/status/collectors/index.js | 2 +- src/legacy/server/status/index.js | 10 +- .../status/routes/api/register_stats.js | 17 +- src/legacy/server/usage/README.md | 92 -------- .../server/usage/classes/collector_set.js | 206 ----------------- src/plugins/usage_collection/README.md | 139 ++++++++++++ .../usage_collection/common/constants.ts} | 2 +- src/plugins/usage_collection/kibana.json | 7 + .../collector}/__tests__/collector_set.js | 38 ++-- .../server/collector}/collector.js | 7 +- .../server/collector/collector_set.ts | 209 ++++++++++++++++++ .../server/collector/index.ts} | 2 + .../server/collector}/usage_collector.js | 8 +- .../usage_collection/server/config.ts} | 17 +- .../usage_collection/server/index.ts} | 18 +- src/plugins/usage_collection/server/plugin.ts | 55 +++++ x-pack/legacy/plugins/apm/index.ts | 3 +- .../apm/server/lib/apm_telemetry/index.ts | 11 +- .../plugins/apm/server/routes/typings.ts | 8 +- .../canvas/__tests__/fixtures/kibana.js | 6 - x-pack/legacy/plugins/canvas/server/plugin.ts | 2 +- x-pack/legacy/plugins/canvas/server/shim.ts | 5 +- .../plugins/canvas/server/usage/collector.ts | 14 +- ....test.ts => cloud_usage_collector.test.ts} | 52 ++--- ..._collector.ts => cloud_usage_collector.ts} | 25 +-- x-pack/legacy/plugins/cloud/index.js | 5 +- x-pack/legacy/plugins/file_upload/index.js | 10 +- .../plugins/file_upload/server/plugin.js | 13 +- .../telemetry/file_upload_usage_collector.ts | 28 +++ .../file_upload/server/telemetry/index.ts | 2 +- .../plugins/infra/server/kibana.index.ts | 9 +- .../infra/server/usage/usage_collector.ts | 12 +- x-pack/legacy/plugins/lens/index.ts | 4 +- x-pack/legacy/plugins/lens/server/plugin.tsx | 27 +-- .../plugins/lens/server/usage/collectors.ts | 29 +-- .../legacy/plugins/lens/server/usage/task.ts | 6 +- x-pack/legacy/plugins/maps/index.js | 4 +- .../maps_telemetry/maps_usage_collector.js | 11 +- .../plugins/maps/server/test_utils/index.js | 6 - x-pack/legacy/plugins/ml/index.ts | 2 +- .../ml_telemetry/make_ml_usage_collector.ts | 15 +- .../plugins/ml/server/new_platform/plugin.ts | 18 +- x-pack/legacy/plugins/monitoring/index.js | 6 +- .../server/kibana_monitoring/bulk_uploader.js | 30 +-- .../collectors/get_kibana_usage_collector.js | 4 +- .../collectors/get_ops_stats_collector.js | 5 +- .../collectors/get_settings_collector.js | 4 +- .../kibana_monitoring/collectors/index.js | 14 +- .../server/kibana_monitoring/index.js | 1 + .../__test__/get_collection_status.js | 11 + .../setup/collection/get_collection_status.js | 16 +- .../plugins/monitoring/server/plugin.js | 22 +- .../__tests__/get_cluster_uuids.js | 21 +- .../legacy/plugins/oss_telemetry/index.d.ts | 6 - x-pack/legacy/plugins/oss_telemetry/index.js | 3 +- .../server/lib/collectors/index.ts | 5 +- .../register_usage_collector.ts | 11 +- .../plugins/oss_telemetry/test_utils/index.ts | 6 - x-pack/legacy/plugins/reporting/index.ts | 7 +- .../plugins/reporting/server/usage/index.ts | 2 +- ...t.js => reporting_usage_collector.test.js} | 32 +-- ...lector.ts => reporting_usage_collector.ts} | 19 +- x-pack/legacy/plugins/rollup/index.js | 3 +- .../plugins/rollup/server/usage/collector.js | 6 +- x-pack/legacy/plugins/spaces/index.ts | 1 - .../legacy/plugins/upgrade_assistant/index.ts | 3 +- .../lib/telemetry/es_ui_open_apis.test.ts | 6 - .../lib/telemetry/es_ui_reindex_apis.test.ts | 6 - .../server/np_ready/lib/telemetry/index.ts | 2 +- .../lib/telemetry/usage_collector.test.ts | 21 +- .../np_ready/lib/telemetry/usage_collector.ts | 10 +- .../server/np_ready/plugin.ts | 10 +- .../server/np_ready/types.ts | 6 - x-pack/legacy/plugins/uptime/index.ts | 4 +- .../plugins/uptime/server/kibana.index.ts | 10 +- .../lib/adapters/framework/adapter_types.ts | 3 +- .../kibana_telemetry_adapter.test.ts | 16 +- .../telemetry/kibana_telemetry_adapter.ts | 11 +- .../server/routes/api/v1/settings.js | 4 +- x-pack/plugins/spaces/kibana.json | 2 +- ...test.ts => spaces_usage_collector.test.ts} | 25 +-- ...collector.ts => spaces_usage_collector.ts} | 21 +- x-pack/plugins/spaces/server/plugin.ts | 36 +-- .../api/__fixtures__/create_legacy_api.ts | 1 - 112 files changed, 1046 insertions(+), 868 deletions(-) create mode 100644 src/legacy/core_plugins/telemetry/README.md delete mode 100644 src/legacy/server/usage/README.md delete mode 100644 src/legacy/server/usage/classes/collector_set.js create mode 100644 src/plugins/usage_collection/README.md rename src/{legacy/server/usage/lib/index.js => plugins/usage_collection/common/constants.ts} (92%) create mode 100644 src/plugins/usage_collection/kibana.json rename src/{legacy/server/usage/classes => plugins/usage_collection/server/collector}/__tests__/collector_set.js (85%) rename src/{legacy/server/usage/classes => plugins/usage_collection/server/collector}/collector.js (93%) create mode 100644 src/plugins/usage_collection/server/collector/collector_set.ts rename src/{legacy/server/usage/classes/index.js => plugins/usage_collection/server/collector/index.ts} (97%) rename src/{legacy/server/usage/classes => plugins/usage_collection/server/collector}/usage_collector.js (88%) rename src/{legacy/server/usage/lib/get_collector_logger.js => plugins/usage_collection/server/config.ts} (67%) rename src/{legacy/server/usage/index.js => plugins/usage_collection/server/index.ts} (63%) create mode 100644 src/plugins/usage_collection/server/plugin.ts rename x-pack/legacy/plugins/cloud/{get_cloud_usage_collector.test.ts => cloud_usage_collector.test.ts} (56%) rename x-pack/legacy/plugins/cloud/{get_cloud_usage_collector.ts => cloud_usage_collector.ts} (57%) create mode 100644 x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts rename x-pack/legacy/plugins/reporting/server/usage/{get_reporting_usage_collector.test.js => reporting_usage_collector.test.js} (93%) rename x-pack/legacy/plugins/reporting/server/usage/{get_reporting_usage_collector.ts => reporting_usage_collector.ts} (70%) rename x-pack/plugins/spaces/server/lib/{get_spaces_usage_collector.test.ts => spaces_usage_collector.test.ts} (83%) rename x-pack/plugins/spaces/server/lib/{get_spaces_usage_collector.ts => spaces_usage_collector.ts} (88%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e208dc73c7b4b..d567f267afa9d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -77,6 +77,7 @@ /src/dev/i18n @elastic/kibana-stack-services /packages/kbn-analytics/ @elastic/kibana-stack-services /src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services +/src/plugins/usage_collection/ @elastic/kibana-stack-services /x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services /x-pack/legacy/plugins/alerting @elastic/kibana-stack-services /x-pack/legacy/plugins/actions @elastic/kibana-stack-services diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 11a5f33a1b2d8..fbe2740b96108 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -210,3 +210,40 @@ export class Plugin { } } ``` + +### Usage Collection + +For creating and registering a Usage Collector. Collectors would be defined in a separate directory `server/collectors/register.ts`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`. + +```ts +// server/collectors/register.ts +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const myCollector = usageCollection.makeUsageCollector({ + type: MY_USAGE_TYPE, + fetch: async (callCluster: CallCluster) => { + + // query ES and get some data + // summarize the data into a model + // return the modeled object that includes whatever you want to track + + return { + my_objects: { + total: SOME_NUMBER + } + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(myCollector); +} +``` diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index dcb7d7998ff1a..91364071579ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -324,6 +324,7 @@ export default function (kibana) { }, init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; // uuid await manageUuid(server); // routes @@ -338,8 +339,8 @@ export default function (kibana) { registerKqlTelemetryApi(server); registerFieldFormats(server); registerTutorials(server); - makeKQLUsageCollector(server); - registerCspCollector(server); + makeKQLUsageCollector(usageCollection, server); + registerCspCollector(usageCollection, server); server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts index 3ff39c1a4eb8c..9890aaf187a13 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts @@ -19,6 +19,7 @@ import { Server } from 'hapi'; import { createCSPRuleString, DEFAULT_CSP_RULES } from '../../../../../server/csp'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export function createCspCollector(server: Server) { return { @@ -42,8 +43,7 @@ export function createCspCollector(server: Server) { }; } -export function registerCspCollector(server: Server): void { - const { collectorSet } = server.usage; - const collector = collectorSet.makeUsageCollector(createCspCollector(server)); - collectorSet.register(collector); +export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void { + const collector = usageCollection.makeUsageCollector(createCspCollector(server)); + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js index 19fb64b7ecc74..6d751a9e9ff45 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js @@ -19,14 +19,14 @@ import { fetchProvider } from './fetch'; -export function makeKQLUsageCollector(server) { +export function makeKQLUsageCollector(usageCollection, server) { const index = server.config().get('kibana.index'); const fetch = fetchProvider(index); - const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({ + const kqlUsageCollector = usageCollection.makeUsageCollector({ type: 'kql', fetch, isReady: () => true, }); - server.usage.collectorSet.register(kqlUsageCollector); + usageCollection.registerCollector(kqlUsageCollector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js index 24f336043d0d1..7737a0fbc2a71 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js @@ -20,29 +20,30 @@ import { makeKQLUsageCollector } from './make_kql_usage_collector'; describe('makeKQLUsageCollector', () => { - let server; let makeUsageCollectorStub; let registerStub; + let usageCollection; beforeEach(() => { makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; server = { - usage: { - collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, - }, config: () => ({ get: () => '.kibana' }) }; }); - it('should call collectorSet.register', () => { - makeKQLUsageCollector(server); + it('should call registerCollector', () => { + makeKQLUsageCollector(usageCollection, server); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = kql', () => { - makeKQLUsageCollector(server); + makeKQLUsageCollector(usageCollection, server); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('kql'); }); diff --git a/src/legacy/core_plugins/telemetry/README.md b/src/legacy/core_plugins/telemetry/README.md new file mode 100644 index 0000000000000..830c08f8e8bed --- /dev/null +++ b/src/legacy/core_plugins/telemetry/README.md @@ -0,0 +1,9 @@ +# Kibana Telemetry Service + +Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + +1. Integrating with the telemetry service to express how to collect usage data (Collecting). +2. Sending a payload of usage data up to Elastic's telemetry cluster. +3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). + +This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 5ae0d5f127eed..9f850fc0fe719 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -27,14 +27,7 @@ import { i18n } from '@kbn/i18n'; import mappings from './mappings.json'; import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask } from './server'; - -import { - createLocalizationUsageCollector, - createTelemetryUsageCollector, - createUiMetricUsageCollector, - createTelemetryPluginUsageCollector, -} from './server/collectors'; +import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server'; const ENDPOINT_VERSION = 'v2'; @@ -123,6 +116,7 @@ const telemetry = (kibana: any) => { fetcherTask.start(); }, init(server: Server) { + const { usageCollection } = server.newPlatform.setup.plugins; const initializerContext = { env: { packageInfo: { @@ -149,12 +143,11 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; - telemetryPlugin(initializerContext).setup(coreSetup); - // register collectors - server.usage.collectorSet.register(createTelemetryPluginUsageCollector(server)); - server.usage.collectorSet.register(createLocalizationUsageCollector(server)); - server.usage.collectorSet.register(createTelemetryUsageCollector(server)); - server.usage.collectorSet.register(createUiMetricUsageCollector(server)); + const pluginsSetup: PluginsSetup = { + usageCollection, + }; + + telemetryPlugin(initializerContext).setup(coreSetup, pluginsSetup, server); }, }); }; diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 799d9f4ee9c8b..933c249cd7279 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -19,6 +19,7 @@ import { encryptTelemetry } from './collectors'; import { CallCluster } from '../../elasticsearch'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; export type EncryptedStatsGetterConfig = { unencrypted: false } & { server: any; @@ -37,6 +38,7 @@ export interface ClusterDetails { } export interface StatsCollectionConfig { + usageCollection: UsageCollectionSetup; callCluster: CallCluster; server: any; start: string; @@ -112,7 +114,8 @@ export class TelemetryCollectionManager { ? (...args: any[]) => callWithRequest(config.req, ...args) : callWithInternalUser; - return { server, callCluster, start, end }; + const { usageCollection } = server.newPlatform.setup.plugins; + return { server, callCluster, start, end, usageCollection }; }; private getOptInStatsForCollection = async ( diff --git a/src/legacy/core_plugins/telemetry/server/collectors/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/index.ts index f963ecec0477c..2f2a53278117b 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/index.ts @@ -18,7 +18,7 @@ */ export { encryptTelemetry } from './encryption'; -export { createTelemetryUsageCollector } from './usage'; -export { createUiMetricUsageCollector } from './ui_metric'; -export { createLocalizationUsageCollector } from './localization'; -export { createTelemetryPluginUsageCollector } from './telemetry_plugin'; +export { registerTelemetryUsageCollector } from './usage'; +export { registerUiMetricUsageCollector } from './ui_metric'; +export { registerLocalizationUsageCollector } from './localization'; +export { registerTelemetryPluginUsageCollector } from './telemetry_plugin'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts index 3b289752ce39f..71026b026263f 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createLocalizationUsageCollector } from './telemetry_localization_collector'; +export { registerLocalizationUsageCollector } from './telemetry_localization_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts index 74c93931096b2..191565187be14 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts @@ -21,6 +21,7 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { getIntegrityHashes, Integrities } from './file_integrity'; import { KIBANA_LOCALIZATION_STATS_TYPE } from '../../../common/constants'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export interface UsageStats { locale: string; integrities: Integrities; @@ -51,15 +52,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function createLocalizationUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerLocalizationUsageCollector( + usageCollection: UsageCollectionSetup, + server: any +) { + const collector = usageCollection.makeUsageCollector({ type: KIBANA_LOCALIZATION_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts index e96c47741f79c..631a37e674c4e 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; +export { registerTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts index a172ba7dc6955..5e25538cbad80 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts @@ -20,6 +20,8 @@ import { TELEMETRY_STATS_TYPE } from '../../../common/constants'; import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository'; import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../telemetry_config'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; + export interface TelemetryUsageStats { opt_in_status?: boolean | null; usage_fetcher?: 'browser' | 'server'; @@ -61,15 +63,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function createTelemetryPluginUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerTelemetryPluginUsageCollector( + usageCollection: UsageCollectionSetup, + server: any +) { + const collector = usageCollection.makeUsageCollector({ type: TELEMETRY_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts index e1ac7a1f5af12..013db526211e1 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; +export { registerUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index fa3159669c33c..73157abce8629 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -18,10 +18,10 @@ */ import { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; -export function createUiMetricUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerUiMetricUsageCollector(usageCollection: UsageCollectionSetup, server: any) { + const collector = usageCollection.makeUsageCollector({ type: UI_METRIC_USAGE_TYPE, fetch: async () => { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; @@ -55,4 +55,6 @@ export function createUiMetricUsageCollector(server: any) { }, isReady: () => true, }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts index a1b3d5a7b1982..3ef9eed3c1265 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createTelemetryUsageCollector } from './telemetry_usage_collector'; +export { registerTelemetryUsageCollector } from './telemetry_usage_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts index 3806dfc77120f..2b2e946198e0a 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts @@ -25,20 +25,15 @@ import { createTelemetryUsageCollector, isFileReadable, readTelemetryFile, - KibanaHapiServer, MAX_FILE_SIZE, } from './telemetry_usage_collector'; -const getMockServer = (): KibanaHapiServer => - ({ - usage: { - collectorSet: { makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg) }, - }, - } as KibanaHapiServer & Server); +const mockUsageCollector = () => ({ + makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg), +}); -const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { +const serverWithConfig = (configPath: string): Server => { return { - ...getMockServer(), config: () => ({ get: (key: string) => { if (key !== 'telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') { @@ -48,7 +43,7 @@ const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { return configPath; }, }), - } as KibanaHapiServer & Server; + } as Server; }; describe('telemetry_usage_collector', () => { @@ -130,14 +125,15 @@ describe('telemetry_usage_collector', () => { }); describe('createTelemetryUsageCollector', () => { - test('calls `collectorSet.makeUsageCollector`', async () => { + test('calls `makeUsageCollector`', async () => { // note: it uses the file's path to get the directory, then looks for 'telemetry.yml' // exclusively, which is indirectly tested by passing it the wrong "file" in the same // dir - const server: KibanaHapiServer & Server = serverWithConfig(tempFiles.unreadable); + const server: Server = serverWithConfig(tempFiles.unreadable); // the `makeUsageCollector` is mocked above to return the argument passed to it - const collectorOptions = createTelemetryUsageCollector(server); + const usageCollector = mockUsageCollector() as any; + const collectorOptions = createTelemetryUsageCollector(usageCollector, server); expect(collectorOptions.type).toBe('static_telemetry'); expect(await collectorOptions.fetch()).toEqual(expectedObject); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index c927453641193..99090cb2fb7ef 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -25,20 +25,13 @@ import { dirname, join } from 'path'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; /** * The maximum file size before we ignore it (note: this limit is arbitrary). */ export const MAX_FILE_SIZE = 10 * 1024; // 10 KB -export interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: (collector: object) => any; - }; - }; -} - /** * Determine if the supplied `path` is readable. * @@ -83,19 +76,11 @@ export async function readTelemetryFile(path: string): Promise true, fetch: async () => { @@ -106,3 +91,11 @@ export function createTelemetryUsageCollector(server: KibanaHapiServer) { }, }); } + +export function registerTelemetryUsageCollector( + usageCollection: UsageCollectionSetup, + server: Server +) { + const collector = createTelemetryUsageCollector(usageCollection, server); + usageCollection.registerCollector(collector); +} diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 02752ca773488..6c62d03adf25c 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -24,7 +24,7 @@ import * as constants from '../common/constants'; export { FetcherTask } from './fetcher'; export { replaceTelemetryInjectedVars } from './telemetry_config'; export { telemetryCollectionManager } from './collection_manager'; - +export { PluginsSetup } from './plugin'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => new TelemetryPlugin(initializerContext); export { constants }; diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts index f2628090c08af..06a974f473498 100644 --- a/src/legacy/core_plugins/telemetry/server/plugin.ts +++ b/src/legacy/core_plugins/telemetry/server/plugin.ts @@ -18,8 +18,20 @@ */ import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Server } from 'hapi'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; +import { + registerUiMetricUsageCollector, + registerTelemetryUsageCollector, + registerLocalizationUsageCollector, + registerTelemetryPluginUsageCollector, +} from './collectors'; + +export interface PluginsSetup { + usageCollection: UsageCollectionSetup; +} export class TelemetryPlugin { private readonly currentKibanaVersion: string; @@ -28,9 +40,15 @@ export class TelemetryPlugin { this.currentKibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup, server: Server) { const currentKibanaVersion = this.currentKibanaVersion; + registerCollection(); registerRoutes({ core, currentKibanaVersion }); + + registerTelemetryPluginUsageCollector(usageCollection, server); + registerLocalizationUsageCollector(usageCollection, server); + registerTelemetryUsageCollector(usageCollection, server); + registerUiMetricUsageCollector(usageCollection, server); } } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index 4cbdf18df4a74..140204ac5ab49 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -29,7 +29,12 @@ import { handleLocalStats, } from '../get_local_stats'; -const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ +const mockUsageCollection = (kibanaUsage = {}) => ({ + bulkFetch: () => kibanaUsage, + toObject: data => data, +}); + +const getMockServer = (getCluster = sinon.stub()) => ({ log(tags, message) { console.log({ tags, message }); }, @@ -43,7 +48,6 @@ const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ } }; }, - usage: { collectorSet: { bulkFetch: () => kibanaUsage, toObject: data => data } }, plugins: { elasticsearch: { getCluster }, }, @@ -155,15 +159,16 @@ describe('get_local_stats', () => { describe.skip('getLocalStats', () => { it('returns expected object without xpack data when X-Pack fails to respond', async () => { const callClusterUsageFailed = sinon.stub(); - + const usageCollection = mockUsageCollection(); mockGetLocalStats( callClusterUsageFailed, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - const result = await getLocalStats({ + const result = await getLocalStats([], { server: getMockServer(), callCluster: callClusterUsageFailed, + usageCollection, }); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); @@ -178,15 +183,16 @@ describe('get_local_stats', () => { it('returns expected object with xpack and kibana data', async () => { const callCluster = sinon.stub(); - + const usageCollection = mockUsageCollection(kibana); mockGetLocalStats( callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - const result = await getLocalStats({ - server: getMockServer(callCluster, kibana), + const result = await getLocalStats([], { + server: getMockServer(callCluster), + usageCollection, callCluster, }); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js index 051ef370fcde5..236dd046148f6 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js @@ -47,12 +47,7 @@ export function handleKibanaStats(server, response) { }; } -/* - * Check user privileges for read access to monitoring - * Pass callWithInternalUser to bulkFetchUsage - */ -export async function getKibana(server, callWithInternalUser) { - const { collectorSet } = server.usage; - const usage = await collectorSet.bulkFetch(callWithInternalUser); - return collectorSet.toObject(usage); +export async function getKibana(usageCollection, callWithInternalUser) { + const usage = await usageCollection.bulkFetch(callWithInternalUser); + return usageCollection.toObject(usage); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index e11c6b1277d5b..a4ea2eb534226 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -55,13 +55,14 @@ export function handleLocalStats(server: any, clusterInfo: any, clusterStats: an * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ export const getLocalStats: StatsGetter = async (clustersDetails, config) => { - const { server, callCluster } = config; + const { server, callCluster, usageCollection } = config; + return await Promise.all( clustersDetails.map(async clustersDetail => { const [clusterInfo, clusterStats, kibana] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) - getKibana(server, callCluster), + getKibana(usageCollection, callCluster), ]); return handleLocalStats(server, clusterInfo, clusterStats, kibana); }) diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 9cc4e30d4252d..6f2730476956e 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -38,7 +38,7 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/serve import { SavedObjectsManagement } from '../../core/server/saved_objects/management'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; - +import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; import { CapabilitiesModifier } from './capabilities'; import { IndexPatternsServiceFactory } from './index_patterns'; import { Capabilities } from '../../core/public'; @@ -67,7 +67,6 @@ declare module 'hapi' { config: () => KibanaConfig; indexPatternsServiceFactory: IndexPatternsServiceFactory; savedObjects: SavedObjectsLegacyService; - usage: { collectorSet: any }; injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void; getHiddenUiAppById(appId: string): UiApp; registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void; @@ -101,6 +100,11 @@ declare module 'hapi' { type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; +export interface PluginsSetup { + usageCollection: UsageCollectionSetup; + [key: string]: object; +} + // eslint-disable-next-line import/no-default-export export default class KbnServer { public readonly newPlatform: { @@ -120,7 +124,7 @@ export default class KbnServer { }; setup: { core: CoreSetup; - plugins: Record; + plugins: PluginsSetup; }; start: { core: CoreSetup; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index f7ed56b10c267..e5f182c931d80 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -28,7 +28,6 @@ import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; import warningsMixin from './warnings'; -import { usageMixin } from './usage'; import { statusMixin } from './status'; import pidMixin from './pid'; import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; @@ -94,7 +93,6 @@ export default class KbnServer { loggingMixin, configDeprecationWarningsMixin, warningsMixin, - usageMixin, statusMixin, // writes pid file diff --git a/src/legacy/server/sample_data/usage/collector.ts b/src/legacy/server/sample_data/usage/collector.ts index 8561a6c3f1007..bcb5e7be2597a 100644 --- a/src/legacy/server/sample_data/usage/collector.ts +++ b/src/legacy/server/sample_data/usage/collector.ts @@ -17,26 +17,25 @@ * under the License. */ -import * as Hapi from 'hapi'; +import { Server } from 'hapi'; import { fetchProvider } from './collector_fetch'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; -interface KbnServer extends Hapi.Server { - usage: any; -} - -export function makeSampleDataUsageCollector(server: KbnServer) { +export function makeSampleDataUsageCollector( + usageCollection: UsageCollectionSetup, + server: Server +) { let index: string; try { index = server.config().get('kibana.index'); } catch (err) { return; // kibana plugin is not enabled (test environment) } + const collector = usageCollection.makeUsageCollector({ + type: 'sample-data', + fetch: fetchProvider(index), + isReady: () => true, + }); - server.usage.collectorSet.register( - server.usage.collectorSet.makeUsageCollector({ - type: 'sample-data', - fetch: fetchProvider(index), - isReady: () => true, - }) - ); + usageCollection.registerCollector(collector); } diff --git a/src/legacy/server/status/collectors/get_ops_stats_collector.js b/src/legacy/server/status/collectors/get_ops_stats_collector.js index aded85384fd85..116e588c5ade6 100644 --- a/src/legacy/server/status/collectors/get_ops_stats_collector.js +++ b/src/legacy/server/status/collectors/get_ops_stats_collector.js @@ -35,9 +35,8 @@ import { getKibanaInfoForStats } from '../lib'; * the metrics. * See PR comment in https://github.com/elastic/kibana/pull/20577/files#r202416647 */ -export function getOpsStatsCollector(server, kbnServer) { - const { collectorSet } = server.usage; - return collectorSet.makeStatsCollector({ +export function getOpsStatsCollector(usageCollection, server, kbnServer) { + return usageCollection.makeStatsCollector({ type: KIBANA_STATS_TYPE, fetch: () => { return { @@ -49,3 +48,10 @@ export function getOpsStatsCollector(server, kbnServer) { ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. }); } + +export function registerOpsStatsCollector(usageCollection, server, kbnServer) { + if (usageCollection) { + const collector = getOpsStatsCollector(usageCollection, server, kbnServer); + usageCollection.registerCollector(collector); + } +} diff --git a/src/legacy/server/status/collectors/index.js b/src/legacy/server/status/collectors/index.js index 4310dff7359ef..92d9e601bbb35 100644 --- a/src/legacy/server/status/collectors/index.js +++ b/src/legacy/server/status/collectors/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { getOpsStatsCollector } from './get_ops_stats_collector'; +export { registerOpsStatsCollector } from './get_ops_stats_collector'; diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js index dda20878605e5..ba2f835599bc9 100644 --- a/src/legacy/server/status/index.js +++ b/src/legacy/server/status/index.js @@ -20,17 +20,15 @@ import ServerStatus from './server_status'; import { Metrics } from './lib/metrics'; import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes'; -import { getOpsStatsCollector } from './collectors'; +import { registerOpsStatsCollector } from './collectors'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; import { getOSInfo } from './lib/get_os_info'; export function statusMixin(kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); - - const statsCollector = getOpsStatsCollector(server, kbnServer); - const { collectorSet } = server.usage; - collectorSet.register(statsCollector); + const { usageCollection } = server.newPlatform.setup.plugins; + registerOpsStatsCollector(usageCollection, server, kbnServer); const metrics = new Metrics(config, server); @@ -57,7 +55,7 @@ export function statusMixin(kbnServer, server, config) { // init routes registerStatusPage(kbnServer, server, config); registerStatusApi(kbnServer, server, config); - registerStatsApi(kbnServer, server, config); + registerStatsApi(usageCollection, server, config); // expore shared functionality server.decorate('server', 'getOSInfo', getOSInfo); diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 91272ead1d2c1..366d36860731c 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -29,7 +29,7 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { /* * API for Kibana meta info and accumulated operations stats - * Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data + * Including ?extended in the query string fetches Elasticsearch cluster_uuid and usageCollection data * - Requests to set isExtended = true * GET /api/stats?extended=true * GET /api/stats?extended @@ -37,9 +37,8 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { * - Any other value causes a statusCode 400 response (Bad Request) * Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended */ -export function registerStatsApi(kbnServer, server, config) { +export function registerStatsApi(usageCollection, server, config) { const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - const { collectorSet } = server.usage; const getClusterUuid = async callCluster => { const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid', }); @@ -47,8 +46,8 @@ export function registerStatsApi(kbnServer, server, config) { }; const getUsage = async callCluster => { - const usage = await collectorSet.bulkFetchUsage(callCluster); - return collectorSet.toObject(usage); + const usage = await usageCollection.bulkFetchUsage(callCluster); + return usageCollection.toObject(usage); }; server.route( @@ -74,7 +73,7 @@ export function registerStatsApi(kbnServer, server, config) { if (isExtended) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const callCluster = (...args) => callWithRequest(req, ...args); - const collectorsReady = await collectorSet.areAllCollectorsReady(); + const collectorsReady = await usageCollection.areAllCollectorsReady(); if (shouldGetUsage && !collectorsReady) { return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); @@ -126,7 +125,7 @@ export function registerStatsApi(kbnServer, server, config) { }; } else { - extended = collectorSet.toApiFieldNames({ + extended = usageCollection.toApiFieldNames({ usage: modifiedUsage, clusterUuid }); @@ -139,12 +138,12 @@ export function registerStatsApi(kbnServer, server, config) { /* kibana_stats gets singled out from the collector set as it is used * for health-checking Kibana and fetch does not rely on fetching data * from ES */ - const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); + const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE); if (!await kibanaStatsCollector.isReady()) { return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); } let kibanaStats = await kibanaStatsCollector.fetch(); - kibanaStats = collectorSet.toApiFieldNames(kibanaStats); + kibanaStats = usageCollection.toApiFieldNames(kibanaStats); return { ...kibanaStats, diff --git a/src/legacy/server/usage/README.md b/src/legacy/server/usage/README.md deleted file mode 100644 index 5c4bcc05bbc38..0000000000000 --- a/src/legacy/server/usage/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Kibana Telemetry Service - -Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: - -1. Integrating with the telemetry service to express how to collect usage data (Collecting). -2. Sending a payload of usage data up to Elastic's telemetry cluster, once per browser per day (Sending). -3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). - -You, the feature or plugin developer, mainly need to worry about the first meaning: collecting. To integrate with the telemetry services for usage collection of your feature, there are 2 steps: - -1. Create a usage collector using a factory function -2. Register the usage collector with the Telemetry service - -NOTE: To a lesser extent, there's also a need to update the telemetry payload of Kibana stats and telemetry cluster field mappings to include your fields. This part is typically handled not by you, the developer, but different maintainers of the telemetry cluster. Usually, this step just means talk to the Platform team and have them approve your data model or added fields. - -## Creating and Registering Usage Collector - -A usage collector object is an instance of a class called `UsageCollector`. A factory function on `server.usage.collectorSet` object allows you to create an instance of this class. All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. - -Example: - -```js -// create usage collector -const myCollector = server.usage.collectorSet.makeUsageCollector({ - type: MY_USAGE_TYPE, - fetch: async callCluster => { - - // query ES and get some data - // summarize the data into a model - // return the modeled object that includes whatever you want to track - - return { - my_objects: { - total: SOME_NUMBER - } - }; - }, -}); - -// register usage collector -server.usage.collectorSet.register(myCollector); -``` - -Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection. - -The fetch method also might be called through an internal background task on the Kibana server, which currently lives in the `kibana_monitoring` module of the X-Pack Monitoring plugin, that polls for data and uploads it to Elasticsearch through a bulk API exposed by the Monitoring plugin for Elasticsearch. In this case, the `callCluster` method will be the internal system user and will have read privilege over the entire `.kibana` index. - -Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. - - -Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. - -## Update the telemetry payload and telemetry cluster field mappings - -There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. - -As of the time of this writing (pre-6.5.0) there are a few unpleasant realities with this module. Today, this module has to be aware of all the features that have integrated with it, which it does from hard-coding. It does this because at the time of creation, the payload implemented a designed model where X-Pack plugin info went together regardless if it was ES-specific or Kibana-specific. In hindsight, all the Kibana data could just be put together, X-Pack or not, which it could do in a generic way. This is a known problem and a solution will be implemented in an upcoming refactoring phase, as this would break the contract for model of data sent in the payload. - -The second reality is that new fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster. - -## Testing - -There are a few ways you can test that your usage collector is working properly. - -1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. -2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: - - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. - - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. ✳ - - The dev script in x-pack can be run on the command-line with: - ``` - cd x-pack - node scripts/api_debug.js telemetry --host=http://localhost:5601 - ``` - Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well. - - Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0 -3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields. -4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload. - -✳ At the time of this writing, there is an open issue that in the sending phase, Kibana usage collectors are not "live-pulled" from Kibana API endpoints if Monitoring is disabled. The implementation on this depends on a new secure way to live-pull the data from the end-user's browser, as it would not be appropriate to supply only partial data if the logged-in user only has partial access to `.kibana`. - -## FAQ - -1. **Can telemetry track UI interactions, such as button click?** - Brief answer: no. Telemetry collection happens on the server-side so the usage data will only include information that the server-side is aware of. There is no generic way to do this today, but UI-interaction KPIs can be tracked with a custom server endpoint that gets called for tracking when the UI event happens. -2. **Does the telemetry service have a hook that I can call whenever some event happens in my feature?** - Brief answer: no. Telemetry collection is a fetch model, not a push model. Telemetry fetches info from your collector. -3. **How should I design my data model?** - Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. -4. **Can the telemetry payload include dynamic fields?** - Yes. When you talk to the Platform team about new fields being added, point out specifically which properties will have dynamic inner fields. -5. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** - Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. diff --git a/src/legacy/server/usage/classes/collector_set.js b/src/legacy/server/usage/classes/collector_set.js deleted file mode 100644 index 5a86992f0af71..0000000000000 --- a/src/legacy/server/usage/classes/collector_set.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { snakeCase } from 'lodash'; -import { getCollectorLogger } from '../lib'; -import { Collector } from './collector'; -import { UsageCollector } from './usage_collector'; - -let _waitingForAllCollectorsTimestamp = null; - -/* - * A collector object has types registered into it with the register(type) - * function. Each type that gets registered defines how to fetch its own data - * and optionally, how to combine it into a unified payload for bulk upload. - */ -export class CollectorSet { - /* - * @param {Object} server - server object - * @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance - */ - constructor(server, collectors = [], config = null) { - this._log = getCollectorLogger(server); - this._collectors = collectors; - - /* - * Helper Factory methods - * Define as instance properties to allow enclosing the server object - */ - this.makeStatsCollector = options => new Collector(server, options); - this.makeUsageCollector = options => new UsageCollector(server, options); - this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray, config); - - this._maximumWaitTimeForAllCollectorsInS = config ? config.get('stats.maximumWaitTimeForAllCollectorsInS') : 60; - } - - /* - * @param collector {Collector} collector object - */ - register(collector) { - // check instanceof - if (!(collector instanceof Collector)) { - throw new Error('CollectorSet can only have Collector instances registered'); - } - - this._collectors.push(collector); - - if (collector.init) { - this._log.debug(`Initializing ${collector.type} collector`); - collector.init(); - } - } - - getCollectorByType(type) { - return this._collectors.find(c => c.type === type); - } - - // isUsageCollector(x: UsageCollector | any): x is UsageCollector { - isUsageCollector(x) { - return x instanceof UsageCollector; - } - - async areAllCollectorsReady(collectorSet = this) { - if (!(collectorSet instanceof CollectorSet)) { - throw new Error(`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet); - } - - const collectorTypesNotReady = []; - let allReady = true; - await collectorSet.asyncEach(async collector => { - if (!await collector.isReady()) { - allReady = false; - collectorTypesNotReady.push(collector.type); - } - }); - - if (!allReady && this._maximumWaitTimeForAllCollectorsInS >= 0) { - const nowTimestamp = +new Date(); - _waitingForAllCollectorsTimestamp = _waitingForAllCollectorsTimestamp || nowTimestamp; - const timeWaitedInMS = nowTimestamp - _waitingForAllCollectorsTimestamp; - const timeLeftInMS = (this._maximumWaitTimeForAllCollectorsInS * 1000) - timeWaitedInMS; - if (timeLeftInMS <= 0) { - this._log.debug(`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` - + `but we have waited the required ` - + `${this._maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`); - return true; - } else { - this._log.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); - } - } else { - _waitingForAllCollectorsTimestamp = null; - } - - return allReady; - } - - /* - * Call a bunch of fetch methods and then do them in bulk - * @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors - */ - async bulkFetch(callCluster, collectorSet = this) { - if (!(collectorSet instanceof CollectorSet)) { - throw new Error(`bulkFetch method given bad collectorSet parameter: ` + typeof collectorSet); - } - - const responses = []; - await collectorSet.asyncEach(async collector => { - this._log.debug(`Fetching data from ${collector.type} collector`); - try { - responses.push({ - type: collector.type, - result: await collector.fetchInternal(callCluster) - }); - } - catch (err) { - this._log.warn(err); - this._log.warn(`Unable to fetch data from ${collector.type} collector`); - } - }); - return responses; - } - - /* - * @return {new CollectorSet} - */ - getFilteredCollectorSet(filter) { - const filtered = this._collectors.filter(filter); - return this._makeCollectorSetFromArray(filtered); - } - - async bulkFetchUsage(callCluster) { - const usageCollectors = this.getFilteredCollectorSet(c => c instanceof UsageCollector); - return this.bulkFetch(callCluster, usageCollectors); - } - - // convert an array of fetched stats results into key/object - toObject(statsData) { - if (!statsData) return {}; - return statsData.reduce((accumulatedStats, { type, result }) => { - return { - ...accumulatedStats, - [type]: result, - }; - }, {}); - } - - // rename fields to use api conventions - toApiFieldNames(apiData) { - const getValueOrRecurse = value => { - if (value == null || typeof value !== 'object') { - return value; - } else { - return this.toApiFieldNames(value); // recurse - } - }; - - // handle array and return early, or return a reduced object - - if (Array.isArray(apiData)) { - return apiData.map(getValueOrRecurse); - } - - return Object.keys(apiData).reduce((accum, field) => { - const value = apiData[field]; - let newName = field; - newName = snakeCase(newName); - newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m - newName = newName.replace('_in_bytes', '_bytes'); - newName = newName.replace('_in_millis', '_ms'); - - return { - ...accum, - [newName]: getValueOrRecurse(value), - }; - }, {}); - } - - map(mapFn) { - return this._collectors.map(mapFn); - } - - some(someFn) { - return this._collectors.some(someFn); - } - - async asyncEach(eachFn) { - for (const collector of this._collectors) { - await eachFn(collector); - } - } -} diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md new file mode 100644 index 0000000000000..4502e1a6ceacf --- /dev/null +++ b/src/plugins/usage_collection/README.md @@ -0,0 +1,139 @@ +# Kibana Usage Collection Service + +Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + +1. Create a usage collector. +2. Register the usage collector. + +## Creating and Registering Usage Collector + +All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. + +### New Platform: + +1. Make sure `usageCollection` is in your optional Plugins: + +```json +// plugin/kibana.json +{ + "id": "...", + "optionalPlugins": ["usageCollection"] +} +``` + +2. Register Usage collector in the `setup` function: + +```ts +// server/plugin.ts +class Plugin { + setup(core, plugins) { + registerMyPluginUsageCollector(plugins.usageCollection); + } +} +``` + +3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`. + +```ts +// server/collectors/register.ts +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const myCollector = usageCollection.makeUsageCollector({ + type: MY_USAGE_TYPE, + fetch: async (callCluster: CallCluster) => { + + // query ES and get some data + // summarize the data into a model + // return the modeled object that includes whatever you want to track + + return { + my_objects: { + total: SOME_NUMBER + } + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(myCollector); +} +``` + +Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection. + +Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. + +### Migrating to NP from Legacy Plugins: + +Pass `usageCollection` to the setup NP plugin setup function under plugins. Inside the `setup` function call the `registerCollector` like what you'd do in the NP example above. + +```js +// index.js +export const myPlugin = (kibana: any) => { + return new kibana.Plugin({ + init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; + const plugins = { + usageCollection, + }; + plugin(initializerContext).setup(core, plugins); + } + }); +} +``` + +### Legacy Plugins: + +Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. + +```js +// index.js +export const myPlugin = (kibana: any) => { + return new kibana.Plugin({ + init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; + registerMyPluginUsageCollector(usageCollection); + } + }); +} +``` + +## Update the telemetry payload and telemetry cluster field mappings + +There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. + +New fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster. + +## Testing + +There are a few ways you can test that your usage collector is working properly. + +1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. +2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: + - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. + - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. + - The dev script in x-pack can be run on the command-line with: + ``` + cd x-pack + node scripts/api_debug.js telemetry --host=http://localhost:5601 + ``` + Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well. + - Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0 +3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields. +4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload. + +## FAQ + +1. **How should I design my data model?** + Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. +2. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** + Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. diff --git a/src/legacy/server/usage/lib/index.js b/src/plugins/usage_collection/common/constants.ts similarity index 92% rename from src/legacy/server/usage/lib/index.js rename to src/plugins/usage_collection/common/constants.ts index 7db3cd4506503..edd06b171a72c 100644 --- a/src/legacy/server/usage/lib/index.js +++ b/src/plugins/usage_collection/common/constants.ts @@ -17,4 +17,4 @@ * under the License. */ -export { getCollectorLogger } from './get_collector_logger'; +export const KIBANA_STATS_TYPE = 'kibana_stats'; diff --git a/src/plugins/usage_collection/kibana.json b/src/plugins/usage_collection/kibana.json new file mode 100644 index 0000000000000..145cd89ff884d --- /dev/null +++ b/src/plugins/usage_collection/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "usageCollection", + "configPath": ["usageCollection"], + "version": "kibana", + "server": true, + "ui": false +} diff --git a/src/legacy/server/usage/classes/__tests__/collector_set.js b/src/plugins/usage_collection/server/collector/__tests__/collector_set.js similarity index 85% rename from src/legacy/server/usage/classes/__tests__/collector_set.js rename to src/plugins/usage_collection/server/collector/__tests__/collector_set.js index 5cf18a8a15200..a2e400b876ff7 100644 --- a/src/legacy/server/usage/classes/__tests__/collector_set.js +++ b/src/plugins/usage_collection/server/collector/__tests__/collector_set.js @@ -24,22 +24,25 @@ import { Collector } from '../collector'; import { CollectorSet } from '../collector_set'; import { UsageCollector } from '../usage_collector'; +const mockLogger = () => ({ + debug: sinon.spy(), + warn: sinon.spy(), +}); + describe('CollectorSet', () => { describe('registers a collector set and runs lifecycle events', () => { - let server; let init; let fetch; - beforeEach(() => { - server = { log: sinon.spy() }; init = noop; fetch = noop; }); it('should throw an error if non-Collector type of object is registered', () => { - const collectors = new CollectorSet(server); + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); const registerPojo = () => { - collectors.register({ + collectors.registerCollector({ type: 'type_collector_test', init, fetch, @@ -53,17 +56,17 @@ describe('CollectorSet', () => { it('should log debug status of fetching from the collector', async () => { const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); - const collectors = new CollectorSet(server); - collectors.register(new Collector(server, { + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + collectors.registerCollector(new Collector(logger, { type: 'MY_TEST_COLLECTOR', fetch: caller => caller() })); const result = await collectors.bulkFetch(mockCallCluster); - const calls = server.log.getCalls(); + const calls = logger.debug.getCalls(); expect(calls.length).to.be(1); expect(calls[0].args).to.eql([ - ['debug', 'stats-collection'], 'Fetching data from MY_TEST_COLLECTOR collector', ]); expect(result).to.eql([{ @@ -74,8 +77,9 @@ describe('CollectorSet', () => { it('should gracefully handle a collector fetch method throwing an error', async () => { const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); - const collectors = new CollectorSet(server); - collectors.register(new Collector(server, { + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + collectors.registerCollector(new Collector(logger, { type: 'MY_TEST_COLLECTOR', fetch: () => new Promise((_resolve, reject) => reject()) })); @@ -95,7 +99,8 @@ describe('CollectorSet', () => { let collectorSet; beforeEach(() => { - collectorSet = new CollectorSet(); + const logger = mockLogger(); + collectorSet = new CollectorSet({ logger }); }); it('should snake_case and convert field names to api standards', () => { @@ -161,14 +166,13 @@ describe('CollectorSet', () => { }); describe('isUsageCollector', () => { - const server = { }; const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {} }; it('returns true only for UsageCollector instances', () => { - const collectors = new CollectorSet(server); - - const usageCollector = new UsageCollector(server, collectorOptions); - const collector = new Collector(server, collectorOptions); + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + const usageCollector = new UsageCollector(logger, collectorOptions); + const collector = new Collector(logger, collectorOptions); const randomClass = new (class Random {}); expect(collectors.isUsageCollector(usageCollector)).to.be(true); expect(collectors.isUsageCollector(collector)).to.be(false); diff --git a/src/legacy/server/usage/classes/collector.js b/src/plugins/usage_collection/server/collector/collector.js similarity index 93% rename from src/legacy/server/usage/classes/collector.js rename to src/plugins/usage_collection/server/collector/collector.js index 40b004f51e49a..ab723edf5b719 100644 --- a/src/legacy/server/usage/classes/collector.js +++ b/src/plugins/usage_collection/server/collector/collector.js @@ -17,18 +17,17 @@ * under the License. */ -import { getCollectorLogger } from '../lib'; export class Collector { /* - * @param {Object} server - server object + * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { + constructor(logger, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); } @@ -39,7 +38,7 @@ export class Collector { throw new Error('Collector must be instantiated with a options.fetch function property'); } - this.log = getCollectorLogger(server); + this.log = logger; Object.assign(this, options); // spread in other properties and mutate "this" diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts new file mode 100644 index 0000000000000..a87accc47535e --- /dev/null +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -0,0 +1,209 @@ +/* + * 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 { snakeCase } from 'lodash'; +import { Logger } from 'kibana/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +// @ts-ignore +import { Collector } from './collector'; +// @ts-ignore +import { UsageCollector } from './usage_collector'; + +interface CollectorSetConfig { + logger: Logger; + maximumWaitTimeForAllCollectorsInS: number; + collectors?: Collector[]; +} + +export class CollectorSet { + private _waitingForAllCollectorsTimestamp?: number; + private logger: Logger; + private readonly maximumWaitTimeForAllCollectorsInS: number; + private collectors: Collector[] = []; + constructor({ logger, maximumWaitTimeForAllCollectorsInS, collectors = [] }: CollectorSetConfig) { + this.logger = logger; + this.collectors = collectors; + this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60; + } + + public makeStatsCollector = (options: any) => { + return new Collector(this.logger, options); + }; + public makeUsageCollector = (options: any) => { + return new UsageCollector(this.logger, options); + }; + + /* + * @param collector {Collector} collector object + */ + public registerCollector = (collector: Collector) => { + // check instanceof + if (!(collector instanceof Collector)) { + throw new Error('CollectorSet can only have Collector instances registered'); + } + + this.collectors.push(collector); + + if (collector.init) { + this.logger.debug(`Initializing ${collector.type} collector`); + collector.init(); + } + }; + + public getCollectorByType = (type: string) => { + return this.collectors.find(c => c.type === type); + }; + + public isUsageCollector = (x: UsageCollector | any): x is UsageCollector => { + return x instanceof UsageCollector; + }; + + public areAllCollectorsReady = async (collectorSet = this) => { + if (!(collectorSet instanceof CollectorSet)) { + throw new Error( + `areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet + ); + } + + const collectorTypesNotReady: string[] = []; + let allReady = true; + for (const collector of collectorSet.collectors) { + if (!(await collector.isReady())) { + allReady = false; + collectorTypesNotReady.push(collector.type); + } + } + + if (!allReady && this.maximumWaitTimeForAllCollectorsInS >= 0) { + const nowTimestamp = +new Date(); + this._waitingForAllCollectorsTimestamp = + this._waitingForAllCollectorsTimestamp || nowTimestamp; + const timeWaitedInMS = nowTimestamp - this._waitingForAllCollectorsTimestamp; + const timeLeftInMS = this.maximumWaitTimeForAllCollectorsInS * 1000 - timeWaitedInMS; + if (timeLeftInMS <= 0) { + this.logger.debug( + `All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` + + `but we have waited the required ` + + `${this.maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.` + ); + return true; + } else { + this.logger.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); + } + } else { + this._waitingForAllCollectorsTimestamp = undefined; + } + + return allReady; + }; + + public bulkFetch = async ( + callCluster: CallCluster, + collectors: Collector[] = this.collectors + ) => { + const responses = []; + for (const collector of collectors) { + this.logger.debug(`Fetching data from ${collector.type} collector`); + try { + responses.push({ + type: collector.type, + result: await collector.fetchInternal(callCluster), + }); + } catch (err) { + this.logger.warn(err); + this.logger.warn(`Unable to fetch data from ${collector.type} collector`); + } + } + + return responses; + }; + + /* + * @return {new CollectorSet} + */ + public getFilteredCollectorSet = (filter: any) => { + const filtered = this.collectors.filter(filter); + return this.makeCollectorSetFromArray(filtered); + }; + + public bulkFetchUsage = async (callCluster: CallCluster) => { + const usageCollectors = this.getFilteredCollectorSet((c: any) => c instanceof UsageCollector); + return await this.bulkFetch(callCluster, usageCollectors.collectors); + }; + + // convert an array of fetched stats results into key/object + public toObject = (statsData: any) => { + if (!statsData) return {}; + return statsData.reduce((accumulatedStats: any, { type, result }: any) => { + return { + ...accumulatedStats, + [type]: result, + }; + }, {}); + }; + + // rename fields to use api conventions + public toApiFieldNames = (apiData: any): any => { + const getValueOrRecurse = (value: any) => { + if (value == null || typeof value !== 'object') { + return value; + } else { + return this.toApiFieldNames(value); // recurse + } + }; + + // handle array and return early, or return a reduced object + + if (Array.isArray(apiData)) { + return apiData.map(getValueOrRecurse); + } + + return Object.keys(apiData).reduce((accum, field) => { + const value = apiData[field]; + let newName = field; + newName = snakeCase(newName); + newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m + newName = newName.replace('_in_bytes', '_bytes'); + newName = newName.replace('_in_millis', '_ms'); + + return { + ...accum, + [newName]: getValueOrRecurse(value), + }; + }, {}); + }; + + // TODO: remove + public map = (mapFn: any) => { + return this.collectors.map(mapFn); + }; + + // TODO: remove + public some = (someFn: any) => { + return this.collectors.some(someFn); + }; + + private makeCollectorSetFromArray = (collectors: Collector[]) => { + return new CollectorSet({ + logger: this.logger, + maximumWaitTimeForAllCollectorsInS: this.maximumWaitTimeForAllCollectorsInS, + collectors, + }); + }; +} diff --git a/src/legacy/server/usage/classes/index.js b/src/plugins/usage_collection/server/collector/index.ts similarity index 97% rename from src/legacy/server/usage/classes/index.js rename to src/plugins/usage_collection/server/collector/index.ts index 0d3939e1dc681..962f61474c250 100644 --- a/src/legacy/server/usage/classes/index.js +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -18,5 +18,7 @@ */ export { CollectorSet } from './collector_set'; +// @ts-ignore export { Collector } from './collector'; +// @ts-ignore export { UsageCollector } from './usage_collector'; diff --git a/src/legacy/server/usage/classes/usage_collector.js b/src/plugins/usage_collection/server/collector/usage_collector.js similarity index 88% rename from src/legacy/server/usage/classes/usage_collector.js rename to src/plugins/usage_collection/server/collector/usage_collector.js index 559deaef2ce15..1e2806ea15f3b 100644 --- a/src/legacy/server/usage/classes/usage_collector.js +++ b/src/plugins/usage_collection/server/collector/usage_collector.js @@ -17,20 +17,20 @@ * under the License. */ -import { KIBANA_STATS_TYPE } from '../../status/constants'; +import { KIBANA_STATS_TYPE } from '../../common/constants'; import { Collector } from './collector'; export class UsageCollector extends Collector { /* - * @param {Object} server - server object + * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { - super(server, { type, init, fetch, formatForBulkUpload, ...options }); + constructor(logger, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { + super(logger, { type, init, fetch, formatForBulkUpload, ...options }); /* * Currently, for internal bulk uploading, usage stats are part of diff --git a/src/legacy/server/usage/lib/get_collector_logger.js b/src/plugins/usage_collection/server/config.ts similarity index 67% rename from src/legacy/server/usage/lib/get_collector_logger.js rename to src/plugins/usage_collection/server/config.ts index 023bf6bf635a8..987db1f2b0ff3 100644 --- a/src/legacy/server/usage/lib/get_collector_logger.js +++ b/src/plugins/usage_collection/server/config.ts @@ -17,15 +17,8 @@ * under the License. */ -const LOGGING_TAGS = ['stats-collection']; -/* - * @param {Object} server - * @return {Object} helpful logger object - */ -export function getCollectorLogger(server) { - return { - debug: message => server.log(['debug', ...LOGGING_TAGS], message), - info: message => server.log(['info', ...LOGGING_TAGS], message), - warn: message => server.log(['warning', ...LOGGING_TAGS], message) - }; -} +import { schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + maximumWaitTimeForAllCollectorsInS: schema.number({ defaultValue: 60 }), +}); diff --git a/src/legacy/server/usage/index.js b/src/plugins/usage_collection/server/index.ts similarity index 63% rename from src/legacy/server/usage/index.js rename to src/plugins/usage_collection/server/index.ts index 2a02070a55f95..33a1a0adc6713 100644 --- a/src/legacy/server/usage/index.js +++ b/src/plugins/usage_collection/server/index.ts @@ -17,15 +17,11 @@ * under the License. */ -import { CollectorSet } from './classes'; +import { PluginInitializerContext } from '../../../../src/core/server'; +import { Plugin } from './plugin'; +import { ConfigSchema } from './config'; -export function usageMixin(kbnServer, server, config) { - const collectorSet = new CollectorSet(server, undefined, config); - - /* - * expose the collector set object on the server - * provides factory methods for feature owners to create their own collector objects - * use collectorSet.register(collector) to register your feature's collector object(s) - */ - server.decorate('server', 'usage', { collectorSet }); -} +export { UsageCollectionSetup } from './plugin'; +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext) => + new Plugin(initializerContext); diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts new file mode 100644 index 0000000000000..e8bbc8e512a41 --- /dev/null +++ b/src/plugins/usage_collection/server/plugin.ts @@ -0,0 +1,55 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { TypeOf } from '@kbn/config-schema'; +import { ConfigSchema } from './config'; +import { PluginInitializerContext, Logger } from '../../../../src/core/server'; +import { CollectorSet } from './collector'; + +export type UsageCollectionSetup = CollectorSet; + +export class Plugin { + logger: Logger; + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(): Promise { + const config = await this.initializerContext.config + .create>() + .pipe(first()) + .toPromise(); + + const collectorSet = new CollectorSet({ + logger: this.logger, + maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, + }); + + return collectorSet; + } + + public start() { + this.logger.debug('Starting plugin'); + } + + public stop() { + this.logger.debug('Stopping plugin'); + } +} diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 0cac20ef340d2..1784ed22a2b4d 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -108,7 +108,8 @@ export const apm: LegacyPluginInitializer = kibana => { } } }); - makeApmUsageCollector(server); + const { usageCollection } = server.newPlatform.setup.plugins; + makeApmUsageCollector(usageCollection, server); const apmPlugin = server.newPlatform.setup.plugins .apm as APMPluginContract; diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts index de8846a8f9fb4..ddfb4144d9636 100644 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts @@ -13,6 +13,7 @@ import { APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID } from '../../../common/apm_saved_object_constants'; import { APMLegacyServer } from '../../routes/typings'; +import { UsageCollectionSetup } from '../../../../../../../src/plugins/usage_collection/server'; export function createApmTelementry( agentNames: string[] = [] @@ -43,8 +44,11 @@ export async function storeApmServicesTelemetry( } } -export function makeApmUsageCollector(server: APMLegacyServer) { - const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({ +export function makeApmUsageCollector( + usageCollector: UsageCollectionSetup, + server: APMLegacyServer +) { + const apmUsageCollector = usageCollector.makeUsageCollector({ type: 'apm', fetch: async () => { const internalSavedObjectsClient = getInternalSavedObjectsClient(server); @@ -60,5 +64,6 @@ export function makeApmUsageCollector(server: APMLegacyServer) { }, isReady: () => true }); - server.usage.collectorSet.register(apmUsageCollector); + + usageCollector.registerCollector(apmUsageCollector); } diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index 207fe7fe5da33..9b114eba72626 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -49,13 +49,7 @@ export interface Route< }) => Promise; } -export type APMLegacyServer = Pick & { - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; +export type APMLegacyServer = Pick & { plugins: { elasticsearch: Server['plugins']['elasticsearch']; }; diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js index ed83dbfcb75b7..141beb3d34d78 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js @@ -29,12 +29,6 @@ export class Plugin { has: key => has(config, key), }), route: def => this.routes.push(def), - usage: { - collectorSet: { - makeUsageCollector: () => {}, - register: () => {}, - }, - }, }; const { init } = this.props; diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index 888d9a5f36c32..b338971103381 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -61,7 +61,7 @@ export class Plugin { }, }); - registerCanvasUsageCollector(core, plugins); + registerCanvasUsageCollector(plugins.usageCollection, core); loadSampleData( plugins.sampleData.addSavedObjectsToSampleDataset, plugins.sampleData.addAppLinksToSampleDataset diff --git a/x-pack/legacy/plugins/canvas/server/shim.ts b/x-pack/legacy/plugins/canvas/server/shim.ts index c043f268af8ea..7641e51f14e56 100644 --- a/x-pack/legacy/plugins/canvas/server/shim.ts +++ b/x-pack/legacy/plugins/canvas/server/shim.ts @@ -8,6 +8,7 @@ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { Legacy } from 'kibana'; import { CoreSetup as ExistingCoreSetup } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginSetupContract } from '../../../../plugins/features/server'; export interface CoreSetup { @@ -32,7 +33,7 @@ export interface PluginsSetup { addSavedObjectsToSampleDataset: any; addAppLinksToSampleDataset: any; }; - usage: Legacy.Server['usage']; + usageCollection: UsageCollectionSetup; } export async function createSetupShim( @@ -68,7 +69,7 @@ export async function createSetupShim( // @ts-ignore: Missing from Legacy Server Type addAppLinksToSampleDataset: server.addAppLinksToSampleDataset, }, - usage: server.usage, + usageCollection: server.newPlatform.setup.plugins.usageCollection, }, }; } diff --git a/x-pack/legacy/plugins/canvas/server/usage/collector.ts b/x-pack/legacy/plugins/canvas/server/usage/collector.ts index 7e6ef31d93ba5..ae009f9265722 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/collector.ts @@ -5,7 +5,8 @@ */ import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { CoreSetup, PluginsSetup } from '../shim'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CoreSetup } from '../shim'; // @ts-ignore missing local declaration import { CANVAS_USAGE_TYPE } from '../../common/lib/constants'; import { workpadCollector } from './workpad_collector'; @@ -22,9 +23,12 @@ const collectors: TelemetryCollector[] = [workpadCollector, customElementCollect A usage collector function returns an object derived from current data in the ES Cluster. */ -export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsSetup) { - const kibanaIndex = setup.getServerConfig().get('kibana.index'); - const canvasCollector = plugins.usage.collectorSet.makeUsageCollector({ +export function registerCanvasUsageCollector( + usageCollection: UsageCollectionSetup, + core: CoreSetup +) { + const kibanaIndex = core.getServerConfig().get('kibana.index'); + const canvasCollector = usageCollection.makeUsageCollector({ type: CANVAS_USAGE_TYPE, isReady: () => true, fetch: async (callCluster: CallCluster) => { @@ -42,5 +46,5 @@ export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsS }, }); - plugins.usage.collectorSet.register(canvasCollector); + usageCollection.registerCollector(canvasCollector); } diff --git a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts similarity index 56% rename from x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts rename to x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts index ee80875890480..660cd256cebcd 100644 --- a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts +++ b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts @@ -5,37 +5,39 @@ */ import sinon from 'sinon'; -import { - createCollectorFetch, - getCloudUsageCollector, - KibanaHapiServer, -} from './get_cloud_usage_collector'; +import { Server } from 'hapi'; +import { createCollectorFetch, createCloudUsageCollector } from './cloud_usage_collector'; const CLOUD_ID_STAGING = 'staging:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; const CLOUD_ID = 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; -const getMockServer = (cloudId?: string) => ({ - usage: { collectorSet: { makeUsageCollector: sinon.stub() } }, - config() { - return { - get(path: string) { - switch (path) { - case 'xpack.cloud': - return { id: cloudId }; - default: - throw Error(`server.config().get(${path}) should not be called by this collector.`); - } - }, - }; - }, +const mockUsageCollection = () => ({ + makeUsageCollector: sinon.stub(), }); +const getMockServer = (cloudId?: string) => + ({ + config() { + return { + get(path: string) { + switch (path) { + case 'xpack.cloud': + return { id: cloudId }; + default: + throw Error(`server.config().get(${path}) should not be called by this collector.`); + } + }, + }; + }, + } as Server); + describe('Cloud usage collector', () => { describe('collector', () => { it('returns `isCloudEnabled: false` if `xpack.cloud.id` is not defined', async () => { - const collector = await createCollectorFetch(getMockServer())(); + const mockServer = getMockServer(); + const collector = await createCollectorFetch(mockServer)(); expect(collector.isCloudEnabled).toBe(false); }); @@ -48,11 +50,11 @@ describe('Cloud usage collector', () => { }); }); -describe('getCloudUsageCollector', () => { - it('returns calls `collectorSet.makeUsageCollector`', () => { +describe('createCloudUsageCollector', () => { + it('returns calls `makeUsageCollector`', () => { const mockServer = getMockServer(); - getCloudUsageCollector((mockServer as any) as KibanaHapiServer); - const { makeUsageCollector } = mockServer.usage.collectorSet; - expect(makeUsageCollector.calledOnce).toBe(true); + const usageCollection = mockUsageCollection(); + createCloudUsageCollector(usageCollection as any, mockServer); + expect(usageCollection.makeUsageCollector.calledOnce).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts similarity index 57% rename from x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts rename to x-pack/legacy/plugins/cloud/cloud_usage_collector.ts index 5ce7be59a1c9c..7fdf32144972c 100644 --- a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts +++ b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts @@ -5,21 +5,14 @@ */ import { Server } from 'hapi'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_CLOUD_STATS_TYPE } from './constants'; export interface UsageStats { isCloudEnabled: boolean; } -export interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - }; - }; -} - -export function createCollectorFetch(server: any) { +export function createCollectorFetch(server: Server) { return async function fetchUsageStats(): Promise { const { id } = server.config().get(`xpack.cloud`); @@ -29,15 +22,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function getCloudUsageCollector(server: KibanaHapiServer) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { + return usageCollection.makeUsageCollector({ type: KIBANA_CLOUD_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); } + +export function registerCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { + const collector = createCloudUsageCollector(usageCollection, server); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/legacy/plugins/cloud/index.js b/x-pack/legacy/plugins/cloud/index.js index 0cca122b52316..c2fd35eea5292 100644 --- a/x-pack/legacy/plugins/cloud/index.js +++ b/x-pack/legacy/plugins/cloud/index.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCloudUsageCollector } from './get_cloud_usage_collector'; +import { registerCloudUsageCollector } from './cloud_usage_collector'; export const cloud = kibana => { return new kibana.Plugin({ @@ -40,7 +40,8 @@ export const cloud = kibana => { server.expose('config', { isCloudEnabled: !!config.id }); - server.usage.collectorSet.register(getCloudUsageCollector(server)); + const { usageCollection } = server.newPlatform.setup.plugins; + registerCloudUsageCollector(usageCollection, server); } }); }; diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index 37d4ad80fa2ca..1eefc0afa8f9c 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -22,7 +22,10 @@ export const fileUpload = kibana => { init(server) { const coreSetup = server.newPlatform.setup.core; - const pluginsSetup = {}; + const { usageCollection } = server.newPlatform.setup.plugins; + const pluginsSetup = { + usageCollection, + }; // legacy dependencies const __LEGACY = { @@ -33,11 +36,6 @@ export const fileUpload = kibana => { savedObjects: { getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository }, - usage: { - collectorSet: { - makeUsageCollector: server.usage.collectorSet.makeUsageCollector - } - } }; new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js index 0baef6f8ffa40..d9819bf26faea 100644 --- a/x-pack/legacy/plugins/file_upload/server/plugin.js +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -5,16 +5,13 @@ */ import { getImportRouteHandler } from './routes/file_upload'; -import { getTelemetry, initTelemetry } from './telemetry/telemetry'; import { MAX_BYTES } from '../common/constants/file_import'; - -const TELEMETRY_TYPE = 'fileUploadTelemetry'; +import { registerFileUploadUsageCollector } from './telemetry'; export class FileUploadPlugin { setup(core, plugins, __LEGACY) { const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; - const makeUsageCollector = __LEGACY.usage.collectorSet.makeUsageCollector; // Set up route __LEGACY.route({ @@ -26,11 +23,9 @@ export class FileUploadPlugin { } }); - // Make usage collector - makeUsageCollector({ - type: TELEMETRY_TYPE, - isReady: () => true, - fetch: async () => (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry() + registerFileUploadUsageCollector(plugins.usageCollection, { + elasticsearchPlugin, + getSavedObjectsRepository, }); } } diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts new file mode 100644 index 0000000000000..a2b359ae11638 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getTelemetry, initTelemetry } from './telemetry'; + +const TELEMETRY_TYPE = 'fileUploadTelemetry'; + +export function registerFileUploadUsageCollector( + usageCollection: UsageCollectionSetup, + deps: { + elasticsearchPlugin: any; + getSavedObjectsRepository: any; + } +): void { + const { elasticsearchPlugin, getSavedObjectsRepository } = deps; + const fileUploadUsageCollector = usageCollection.makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => + (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry(), + }); + + usageCollection.registerCollector(fileUploadUsageCollector); +} diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts index 46da040dc34f0..7969dd04ce31f 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './telemetry'; +export { registerFileUploadUsageCollector } from './file_upload_usage_collector'; diff --git a/x-pack/legacy/plugins/infra/server/kibana.index.ts b/x-pack/legacy/plugins/infra/server/kibana.index.ts index 48ef846ec5275..91bcd6be95a75 100644 --- a/x-pack/legacy/plugins/infra/server/kibana.index.ts +++ b/x-pack/legacy/plugins/infra/server/kibana.index.ts @@ -13,11 +13,8 @@ import { UsageCollector } from './usage/usage_collector'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -export interface KbnServer extends Server { - usage: any; -} - -export const initServerWithKibana = (kbnServer: KbnServer) => { +export const initServerWithKibana = (kbnServer: Server) => { + const { usageCollection } = kbnServer.newPlatform.setup.plugins; const libs = compose(kbnServer); initInfraServer(libs); @@ -27,7 +24,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { ); // Register a function with server to manage the collection of usage stats - kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); + UsageCollector.registerUsageCollector(usageCollection); const xpackMainPlugin = kbnServer.plugins.xpack_main; xpackMainPlugin.registerFeature({ diff --git a/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts b/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts index 018c903009bbe..601beddc0a2db 100644 --- a/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts +++ b/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { InfraNodeType } from '../graphql/types'; -import { KbnServer } from '../kibana.index'; - const KIBANA_REPORTING_TYPE = 'infraops'; interface InfraopsSum { @@ -17,10 +16,13 @@ interface InfraopsSum { } export class UsageCollector { - public static getUsageCollector(server: KbnServer) { - const { collectorSet } = server.usage; + public static registerUsageCollector(usageCollection: UsageCollectionSetup): void { + const collector = UsageCollector.getUsageCollector(usageCollection); + usageCollection.registerCollector(collector); + } - return collectorSet.makeUsageCollector({ + public static getUsageCollector(usageCollection: UsageCollectionSetup) { + return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady: () => true, fetch: async () => { diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index d4cea28d14085..a79b9907f6437 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -58,10 +58,12 @@ export const lens: LegacyPluginInitializer = kibana => { // Set up with the new platform plugin lifecycle API. const plugin = lensServerPlugin(); + const { usageCollection } = server.newPlatform.setup.plugins; + plugin.setup(kbnServer.newPlatform.setup.core, { + usageCollection, // Legacy APIs savedObjects: server.savedObjects, - usage: server.usage, config: server.config(), server, }); diff --git a/x-pack/legacy/plugins/lens/server/plugin.tsx b/x-pack/legacy/plugins/lens/server/plugin.tsx index a4c8e9b268df5..0223b90c37046 100644 --- a/x-pack/legacy/plugins/lens/server/plugin.tsx +++ b/x-pack/legacy/plugins/lens/server/plugin.tsx @@ -6,27 +6,22 @@ import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; import { Plugin, CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { setupRoutes } from './routes'; import { registerLensUsageCollector, initializeLensTelemetry } from './usage'; +export interface PluginSetupContract { + savedObjects: SavedObjectsLegacyService; + usageCollection: UsageCollectionSetup; + config: KibanaConfig; + server: Server; +} + export class LensServer implements Plugin<{}, {}, {}, {}> { - setup( - core: CoreSetup, - plugins: { - savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - config: KibanaConfig; - server: Server; - } - ) { + setup(core: CoreSetup, plugins: PluginSetupContract) { setupRoutes(core, plugins); - registerLensUsageCollector(core, plugins); - initializeLensTelemetry(core, plugins); + registerLensUsageCollector(plugins.usageCollection, plugins.server); + initializeLensTelemetry(core, plugins.server); return {}; } diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 94a7c8e0d85c1..274b72c33e59a 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -6,29 +6,17 @@ import moment from 'moment'; import { get } from 'lodash'; -import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; -import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { Server } from 'src/legacy/server/kbn_server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + import { LensUsage, LensTelemetryState } from './types'; -export function registerLensUsageCollector( - core: CoreSetup, - plugins: { - savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - config: KibanaConfig; - server: Server; - } -) { +export function registerLensUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { let isCollectorReady = false; async function determineIfTaskManagerIsReady() { let isReady = false; try { - isReady = await isTaskManagerReady(plugins.server); + isReady = await isTaskManagerReady(server); } catch (err) {} // eslint-disable-line if (isReady) { @@ -39,11 +27,11 @@ export function registerLensUsageCollector( } determineIfTaskManagerIsReady(); - const lensUsageCollector = plugins.usage.collectorSet.makeUsageCollector({ + const lensUsageCollector = usageCollection.makeUsageCollector({ type: 'lens', fetch: async (): Promise => { try { - const docs = await getLatestTaskState(plugins.server); + const docs = await getLatestTaskState(server); // get the accumulated state from the recurring task const state: LensTelemetryState = get(docs, '[0].state'); @@ -75,7 +63,8 @@ export function registerLensUsageCollector( }, isReady: () => isCollectorReady, }); - plugins.usage.collectorSet.register(lensUsageCollector); + + usageCollection.registerCollector(lensUsageCollector); } function addEvents(prevEvents: Record, newEvents: Record) { diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index 03e085cc9e669..feb73538f44f0 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -39,12 +39,12 @@ type ClusterDeleteType = ( options?: CallClusterOptions ) => Promise; -export function initializeLensTelemetry(core: CoreSetup, { server }: { server: Server }) { - registerLensTelemetryTask(core, { server }); +export function initializeLensTelemetry(core: CoreSetup, server: Server) { + registerLensTelemetryTask(core, server); scheduleTasks(server); } -function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server }) { +function registerLensTelemetryTask(core: CoreSetup, server: Server) { const taskManager = server.plugins.task_manager; if (!taskManager) { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 739e98beec10f..c59fbe42a1754 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -101,12 +101,12 @@ export function maps(kibana) { init(server) { const mapsEnabled = server.config().get('xpack.maps.enabled'); - + const { usageCollection } = server.newPlatform.setup.plugins; if (!mapsEnabled) { server.log(['info', 'maps'], 'Maps app disabled by configuration'); return; } - initTelemetryCollection(server); + initTelemetryCollection(usageCollection, server); const xpackMainPlugin = server.plugins.xpack_main; let routesInitialized = false; diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js index c0ac5a781b796..c4d755b5908f0 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -7,10 +7,10 @@ import _ from 'lodash'; import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task'; -export function initTelemetryCollection(server) { +export function initTelemetryCollection(usageCollection, server) { registerMapsTelemetryTask(server); scheduleTask(server); - registerMapsUsageCollector(server); + registerMapsUsageCollector(usageCollection, server); } async function isTaskManagerReady(server) { @@ -81,9 +81,8 @@ export function buildCollectorObj(server) { }; } -export function registerMapsUsageCollector(server) { +export function registerMapsUsageCollector(usageCollection, server) { const collectorObj = buildCollectorObj(server); - const mapsUsageCollector = server.usage.collectorSet - .makeUsageCollector(collectorObj); - server.usage.collectorSet.register(mapsUsageCollector); + const mapsUsageCollector = usageCollection.makeUsageCollector(collectorObj); + usageCollection.registerCollector(mapsUsageCollector); } diff --git a/x-pack/legacy/plugins/maps/server/test_utils/index.js b/x-pack/legacy/plugins/maps/server/test_utils/index.js index 13b7c56d6fc8b..e9f97101759f0 100644 --- a/x-pack/legacy/plugins/maps/server/test_utils/index.js +++ b/x-pack/legacy/plugins/maps/server/test_utils/index.js @@ -40,12 +40,6 @@ export const getMockKbnServer = ( fetch: mockTaskFetch, }, }, - usage: { - collectorSet: { - makeUsageCollector: () => '', - register: () => undefined, - }, - }, config: () => ({ get: () => '' }), log: () => undefined }); diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 3cafa232f0744..90e1e748492cb 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -79,7 +79,6 @@ export const ml = (kibana: any) => { injectUiAppVars: server.injectUiAppVars, http: mlHttpService, savedObjects: server.savedObjects, - usage: server.usage, }; const plugins = { @@ -87,6 +86,7 @@ export const ml = (kibana: any) => { security: server.plugins.security, xpackMain: server.plugins.xpack_main, spaces: server.plugins.spaces, + usageCollection: kbnServer.newPlatform.setup.plugins.usageCollection, ml: this, }; diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index 6bc98ba68f60b..7a9766f36a6ed 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { createMlTelemetry, getSavedObjectsClient, @@ -14,12 +15,11 @@ import { import { UsageInitialization } from '../../new_platform/plugin'; -export function makeMlUsageCollector({ - elasticsearchPlugin, - usage, - savedObjects, -}: UsageInitialization): void { - const mlUsageCollector = usage.collectorSet.makeUsageCollector({ +export function makeMlUsageCollector( + usageCollection: UsageCollectionSetup, + { elasticsearchPlugin, savedObjects }: UsageInitialization +): void { + const mlUsageCollector = usageCollection.makeUsageCollector({ type: 'ml', isReady: () => true, fetch: async (): Promise => { @@ -35,5 +35,6 @@ export function makeMlUsageCollector({ } }, }); - usage.collectorSet.register(mlUsageCollector); + + usageCollection.registerCollector(mlUsageCollector); } diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index b2b697a851703..b789121beebfc 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -10,6 +10,7 @@ import { ServerRoute } from 'hapi'; import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; @@ -68,12 +69,6 @@ export interface MlCoreSetup { injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: any; - register: (collector: any) => void; - }; - }; } export interface MlInitializerContext extends PluginInitializerContext { legacyConfig: KibanaConfig; @@ -84,6 +79,7 @@ export interface PluginsSetup { xpackMain: MlXpackMainPlugin; security: any; spaces: any; + usageCollection: UsageCollectionSetup; // TODO: this is temporary for `mirrorPluginStatus` ml: any; } @@ -98,12 +94,6 @@ export interface RouteInitialization { } export interface UsageInitialization { elasticsearchPlugin: ElasticsearchPlugin; - usage: { - collectorSet: { - makeUsageCollector: any; - register: (collector: any) => void; - }; - }; savedObjects: SavedObjectsLegacyService; } @@ -201,10 +191,8 @@ export class Plugin { savedObjects: core.savedObjects, spacesPlugin: plugins.spaces, }; - const usageInitializationDeps: UsageInitialization = { elasticsearchPlugin: plugins.elasticsearch, - usage: core.usage, savedObjects: core.savedObjects, }; @@ -231,7 +219,7 @@ export class Plugin { fileDataVisualizerRoutes(extendedRouteInitializationDeps); initMlServerLog(logInitializationDeps); - makeMlUsageCollector(usageInitializationDeps); + makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps); } public stop() {} diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index 97046bfb7d5b4..79db8cb920ea3 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -56,9 +56,6 @@ export const monitoring = (kibana) => new kibana.Plugin({ throw `Unknown key '${key}'`; } }), - usage: { - collectorSet: server.usage.collectorSet - }, injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), getOSInfo: server.getOSInfo, @@ -70,11 +67,12 @@ export const monitoring = (kibana) => new kibana.Plugin({ _hapi: server, _kbnServer: this.kbnServer }; - + const { usageCollection } = server.newPlatform.setup.plugins; const plugins = { xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + usageCollection, }; new Plugin().setup(serverFacade, plugins); diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index da23d4b77a323..b0367bc078473 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -68,21 +68,21 @@ export class BulkUploader { /* * Start the interval timer - * @param {CollectorSet} collectorSet object to use for initial the fetch/upload and fetch/uploading on interval + * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval * @return undefined */ - start(collectorSet) { + start(usageCollection) { this._log.info('Starting monitoring stats collection'); - const filterCollectorSet = _collectorSet => { + const filterCollectorSet = _usageCollection => { const successfulUploadInLastDay = this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now(); - return _collectorSet.getFilteredCollectorSet(c => { + return _usageCollection.getFilteredCollectorSet(c => { // this is internal bulk upload, so filter out API-only collectors if (c.ignoreForInternalUploader) { return false; } // Only collect usage data at the same interval as telemetry would (default to once a day) - if (successfulUploadInLastDay && _collectorSet.isUsageCollector(c)) { + if (successfulUploadInLastDay && _usageCollection.isUsageCollector(c)) { return false; } return true; @@ -92,11 +92,11 @@ export class BulkUploader { if (this._timer) { clearInterval(this._timer); } else { - this._fetchAndUpload(filterCollectorSet(collectorSet)); // initial fetch + this._fetchAndUpload(filterCollectorSet(usageCollection)); // initial fetch } this._timer = setInterval(() => { - this._fetchAndUpload(filterCollectorSet(collectorSet)); + this._fetchAndUpload(filterCollectorSet(usageCollection)); }, this._interval); } @@ -121,12 +121,12 @@ export class BulkUploader { } /* - * @param {CollectorSet} collectorSet + * @param {usageCollection} usageCollection * @return {Promise} - resolves to undefined */ - async _fetchAndUpload(collectorSet) { - const collectorsReady = await collectorSet.areAllCollectorsReady(); - const hasUsageCollectors = collectorSet.some(collectorSet.isUsageCollector); + async _fetchAndUpload(usageCollection) { + const collectorsReady = await usageCollection.areAllCollectorsReady(); + const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); if (!collectorsReady) { this._log.debug('Skipping bulk uploading because not all collectors are ready'); if (hasUsageCollectors) { @@ -136,8 +136,8 @@ export class BulkUploader { return; } - const data = await collectorSet.bulkFetch(this._callClusterWithInternalUser); - const payload = this.toBulkUploadFormat(compact(data), collectorSet); + const data = await usageCollection.bulkFetch(this._callClusterWithInternalUser); + const payload = this.toBulkUploadFormat(compact(data), usageCollection); if (payload) { try { @@ -202,7 +202,7 @@ export class BulkUploader { * } * ] */ - toBulkUploadFormat(rawData, collectorSet) { + toBulkUploadFormat(rawData, usageCollection) { if (rawData.length === 0) { return; } @@ -210,7 +210,7 @@ export class BulkUploader { // convert the raw data to a nested object by taking each payload through // its formatter, organizing it per-type const typesNested = rawData.reduce((accum, { type, result }) => { - const { type: uploadType, payload: uploadData } = collectorSet.getCollectorByType(type).formatForBulkUpload(result); + const { type: uploadType, payload: uploadData } = usageCollection.getCollectorByType(type).formatForBulkUpload(result); return defaultsDeep(accum, { [uploadType]: uploadData }); }, {}); // convert the nested object into a flat array, with each payload prefixed diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js index 25efc63fafb5d..5d2ebf8dc2abc 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js @@ -19,8 +19,8 @@ const TYPES = [ /** * Fetches saved object counts by querying the .kibana index */ -export function getKibanaUsageCollector({ collectorSet, config }) { - return collectorSet.makeUsageCollector({ +export function getKibanaUsageCollector(usageCollection, config) { + return usageCollection.makeUsageCollector({ type: KIBANA_USAGE_TYPE, isReady: () => true, async fetch(callCluster) { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js index f1f47761d9f0c..2c0250fb78592 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js @@ -49,14 +49,13 @@ class OpsMonitor { /* * Initialize a collector for Kibana Ops Stats */ -export function getOpsStatsCollector({ +export function getOpsStatsCollector(usageCollection, { elasticsearchPlugin, kbnServerConfig, log, config, getOSInfo, hapiServer, - collectorSet }) { const buffer = opsBuffer({ log, config, getOSInfo }); const interval = kbnServerConfig.get('ops.interval'); @@ -85,7 +84,7 @@ export function getOpsStatsCollector({ }, 5 * 1000); // wait 5 seconds to avoid race condition with reloading logging configuration }); - return collectorSet.makeStatsCollector({ + return usageCollection.makeStatsCollector({ type: KIBANA_STATS_TYPE_MONITORING, init: opsMonitor.start, isReady: () => { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js index bb561ddda42ab..2a56deaad4f8a 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js @@ -46,8 +46,8 @@ export async function checkForEmailValue( } } -export function getSettingsCollector({ config, collectorSet }) { - return collectorSet.makeStatsCollector({ +export function getSettingsCollector(usageCollection, config) { + return usageCollection.makeStatsCollector({ type: KIBANA_SETTINGS_TYPE, isReady: () => true, async fetch(callCluster) { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js index 3c8eb5ebdf2d3..1099a23dea103 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getKibanaUsageCollector } from './get_kibana_usage_collector'; -export { getOpsStatsCollector } from './get_ops_stats_collector'; -export { getSettingsCollector } from './get_settings_collector'; +import { getKibanaUsageCollector } from './get_kibana_usage_collector'; +import { getOpsStatsCollector } from './get_ops_stats_collector'; +import { getSettingsCollector } from './get_settings_collector'; + +export function registerCollectors(usageCollection, collectorsConfigs) { + const { config } = collectorsConfigs; + + usageCollection.registerCollector(getOpsStatsCollector(usageCollection, collectorsConfigs)); + usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, config)); + usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); +} diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js index ae691f49e2b80..c202fe9589ab3 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js @@ -5,3 +5,4 @@ */ export { initBulkUploader } from './init'; +export { registerCollectors } from './collectors'; diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js index bb42dad26786a..36f085c424881 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js @@ -13,6 +13,17 @@ const liveClusterUuid = 'a12'; const mockReq = (searchResult = {}) => { return { server: { + newPlatform: { + setup: { + plugins: { + usageCollection: { + getCollectorByType: () => ({ + isReady: () => false + }), + }, + }, + }, + }, config() { return { get: sinon.stub() diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index d25d8af4aaa20..540de7d1e3a7f 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -273,13 +273,15 @@ function shouldSkipBucket(product, bucket) { return false; } -async function getLiveKibanaInstance(req) { - const { collectorSet } = req.server.usage; - const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); +async function getLiveKibanaInstance(usageCollection) { + if (!usageCollection) { + return null; + } + const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE); if (!await kibanaStatsCollector.isReady()) { return null; } - return collectorSet.toApiFieldNames(await kibanaStatsCollector.fetch()); + return usageCollection.toApiFieldNames(await kibanaStatsCollector.fetch()); } async function getLiveElasticsearchClusterUuid(req) { @@ -341,9 +343,11 @@ async function getLiveElasticsearchCollectionEnabled(req) { * @param {*} skipLiveData Optional and will not make any live api calls if set to true */ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => { + const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); const hasPermissions = await hasNecessaryPermissions(req); + if (!hasPermissions) { return { _meta: { @@ -351,6 +355,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU } }; } + console.log('OKOKOKOK'); const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; @@ -372,7 +377,8 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU const liveEsNodes = skipLiveData || !isLiveCluster ? [] : await getLivesNodes(req); - const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(req); + const { usageCollection } = req.server.newPlatform.setup.plugins; + const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(usageCollection); const indicesBuckets = get(recentDocuments, 'aggregations.indices.buckets', []); const liveClusterInternalCollectionEnabled = await getLiveElasticsearchCollectionEnabled(req); diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index 48a02109a3f6f..97930610e0593 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -9,35 +9,27 @@ import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants' import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; -import { initBulkUploader } from './kibana_monitoring'; +import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; -import { - getKibanaUsageCollector, - getOpsStatsCollector, - getSettingsCollector, -} from './kibana_monitoring/collectors'; - export class Plugin { setup(core, plugins) { const kbnServer = core._kbnServer; const config = core.config(); - const { collectorSet } = core.usage; + const usageCollection = plugins.usageCollection; + registerMonitoringCollection(); /* * Register collector objects for stats to show up in the APIs */ - collectorSet.register(getOpsStatsCollector({ + registerCollectors(usageCollection, { elasticsearchPlugin: plugins.elasticsearch, kbnServerConfig: kbnServer.config, log: core.log, config, getOSInfo: core.getOSInfo, hapiServer: core._hapi, - collectorSet: core.usage.collectorSet, - })); - collectorSet.register(getKibanaUsageCollector({ collectorSet, config })); - collectorSet.register(getSettingsCollector({ collectorSet, config })); - registerMonitoringCollection(); + }); + /* * Instantiate and start the internal background task that calls collector @@ -110,7 +102,7 @@ export class Plugin { const mainMonitoring = xpackMainInfo.feature('monitoring'); const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable() && mainMonitoring.isEnabled(); if (monitoringBulkEnabled) { - bulkUploader.start(collectorSet); + bulkUploader.start(usageCollection); } else { bulkUploader.handleNotEnabled(); } diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js index a0072e52fc7f7..c6bb368745830 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js @@ -8,9 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids'; -// FAILING: https://github.com/elastic/kibana/issues/51371 -describe.skip('get_cluster_uuids', () => { - const callWith = sinon.stub(); +describe('get_cluster_uuids', () => { + const callCluster = sinon.stub(); const size = 123; const server = { config: sinon.stub().returns({ @@ -29,23 +28,23 @@ describe.skip('get_cluster_uuids', () => { } } }; - const expectedUuids = response.aggregations.cluster_uuids.buckets.map(bucket => bucket.key); + const expectedUuids = response.aggregations.cluster_uuids.buckets + .map(bucket => bucket.key) + .map(expectedUuid => ({ clusterUuid: expectedUuid })); const start = new Date(); const end = new Date(); describe('getClusterUuids', () => { it('returns cluster UUIDs', async () => { - callWith.withArgs('search').returns(Promise.resolve(response)); - - expect(await getClusterUuids(server, callWith, start, end)).to.eql(expectedUuids); + callCluster.withArgs('search').returns(Promise.resolve(response)); + expect(await getClusterUuids({ server, callCluster, start, end })).to.eql(expectedUuids); }); }); describe('fetchClusterUuids', () => { it('searches for clusters', async () => { - callWith.returns(Promise.resolve(response)); - - expect(await fetchClusterUuids(server, callWith, start, end)).to.be(response); + callCluster.returns(Promise.resolve(response)); + expect(await fetchClusterUuids({ server, callCluster, start, end })).to.be(response); }); }); @@ -53,13 +52,11 @@ describe.skip('get_cluster_uuids', () => { // filterPath makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusterUuids = handleClusterUuidsResponse({}); - expect(clusterUuids.length).to.be(0); }); it('handles valid response', () => { const clusterUuids = handleClusterUuidsResponse(response); - expect(clusterUuids).to.eql(expectedUuids); }); diff --git a/x-pack/legacy/plugins/oss_telemetry/index.d.ts b/x-pack/legacy/plugins/oss_telemetry/index.d.ts index 012f987627369..1b592dabf2053 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.d.ts +++ b/x-pack/legacy/plugins/oss_telemetry/index.d.ts @@ -54,12 +54,6 @@ export interface HapiServer { }>; }; }; - usage: { - collectorSet: { - register: (collector: any) => void; - makeUsageCollector: (collectorOpts: any) => void; - }; - }; config: () => { get: (prop: string) => any; }; diff --git a/x-pack/legacy/plugins/oss_telemetry/index.js b/x-pack/legacy/plugins/oss_telemetry/index.js index eeee9e18f9112..f86baef020aa2 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.js +++ b/x-pack/legacy/plugins/oss_telemetry/index.js @@ -15,7 +15,8 @@ export const ossTelemetry = (kibana) => { configPrefix: 'xpack.oss_telemetry', init(server) { - registerCollectors(server); + const { usageCollection } = server.newPlatform.setup.plugins; + registerCollectors(usageCollection, server); registerTasks(server); scheduleTasks(server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts index 8b825b13178f2..0121ed4304d26 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HapiServer } from '../../../'; import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; -export function registerCollectors(server: HapiServer) { - registerVisualizationsCollector(server); +export function registerCollectors(usageCollection: UsageCollectionSetup, server: HapiServer) { + registerVisualizationsCollector(usageCollection, server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts index 555c7ac27b49d..09843a6f87ad7 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HapiServer } from '../../../../'; import { getUsageCollector } from './get_usage_collector'; -export function registerVisualizationsCollector(server: HapiServer): void { - const { usage } = server; - const collector = usage.collectorSet.makeUsageCollector(getUsageCollector(server)); - usage.collectorSet.register(collector); +export function registerVisualizationsCollector( + usageCollection: UsageCollectionSetup, + server: HapiServer +): void { + const collector = usageCollection.makeUsageCollector(getUsageCollector(server)); + usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts index 998a1d2beeab1..1cebe78b9c7f0 100644 --- a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts @@ -54,12 +54,6 @@ export const getMockKbnServer = ( fetch: mockTaskFetch, }, }, - usage: { - collectorSet: { - makeUsageCollector: () => '', - register: () => undefined, - }, - }, config: () => mockConfig, log: () => undefined, }); diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index e2b5970d1efb7..9add3accd262f 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -20,7 +20,7 @@ import { import { config as reportingConfig } from './config'; import { logConfiguration } from './log_configuration'; import { createBrowserDriverFactory } from './server/browsers'; -import { getReportingUsageCollector } from './server/usage'; +import { registerReportingUsageCollector } from './server/usage'; import { ReportingConfigOptions, ReportingPluginSpecOptions, ServerFacade } from './types.d'; const kbToBase64Length = (kb: number) => { @@ -76,9 +76,8 @@ export const reporting = (kibana: any) => { async init(server: ServerFacade) { let isCollectorReady = false; // Register a function with server to manage the collection of usage stats - server.usage.collectorSet.register( - getReportingUsageCollector(server, () => isCollectorReady) - ); + const { usageCollection } = server.newPlatform.setup.plugins; + registerReportingUsageCollector(usageCollection, server, () => isCollectorReady); const logger = LevelLogger.createForServer(server, [PLUGIN_ID]); const [exportTypesRegistry, browserFactory] = await Promise.all([ diff --git a/x-pack/legacy/plugins/reporting/server/usage/index.ts b/x-pack/legacy/plugins/reporting/server/usage/index.ts index 91e2a9284550b..141ecb9c77656 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/index.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getReportingUsageCollector } from './get_reporting_usage_collector'; +export { registerReportingUsageCollector } from './reporting_usage_collector'; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js similarity index 93% rename from x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js rename to x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js index 32022c6fa642c..f23f679865146 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ import sinon from 'sinon'; -import { getReportingUsageCollector } from './get_reporting_usage_collector'; +import { getReportingUsageCollector } from './reporting_usage_collector'; -function getServerMock(customization) { +function getMockUsageCollection() { class MockUsageCollector { constructor(_server, { fetch }) { this.fetch = fetch; } } + return { + makeUsageCollector: options => { + return new MockUsageCollector(this, options); + }, + }; +} +function getServerMock(customization) { const getLicenseCheckResults = sinon.stub().returns({}); const defaultServerMock = { plugins: { @@ -44,13 +51,6 @@ function getServerMock(customization) { } }, }), - usage: { - collectorSet: { - makeUsageCollector: options => { - return new MockUsageCollector(this, options); - }, - }, - }, }; return Object.assign(defaultServerMock, customization); } @@ -66,7 +66,8 @@ describe('license checks', () => { .stub() .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); usageStats = await getReportingUsage(callClusterMock); }); @@ -91,7 +92,8 @@ describe('license checks', () => { .stub() .returns('none'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithNoLicenseMock); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock); usageStats = await getReportingUsage(callClusterMock); }); @@ -116,7 +118,9 @@ describe('license checks', () => { .stub() .returns('platinum'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const usageCollection = getMockUsageCollection(); const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, serverWithPlatinumLicenseMock ); usageStats = await getReportingUsage(callClusterMock); @@ -143,7 +147,8 @@ describe('license checks', () => { .stub() .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve({})); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); usageStats = await getReportingUsage(callClusterMock); }); @@ -160,11 +165,12 @@ describe('license checks', () => { describe('data modeling', () => { let getReportingUsage; beforeAll(async () => { + const usageCollection = getMockUsageCollection(); const serverWithPlatinumLicenseMock = getServerMock(); serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon .stub() .returns('platinum'); - ({ fetch: getReportingUsage } = getReportingUsageCollector(serverWithPlatinumLicenseMock)); + ({ fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithPlatinumLicenseMock)); }); test('with normal looking usage data', async () => { diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts similarity index 70% rename from x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts rename to x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts index 5c52193769057..0a7ef0a194434 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; // @ts-ignore untyped module import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; import { ServerFacade, ESCallCluster } from '../../types'; @@ -15,9 +16,12 @@ import { RangeStats } from './types'; * @param {Object} server * @return {Object} kibana usage stats type collection object */ -export function getReportingUsageCollector(server: ServerFacade, isReady: () => boolean) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function getReportingUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerFacade, + isReady: () => boolean +) { + return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady, fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), @@ -41,3 +45,12 @@ export function getReportingUsageCollector(server: ServerFacade, isReady: () => }, }); } + +export function registerReportingUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerFacade, + isReady: () => boolean +) { + const collector = getReportingUsageCollector(usageCollection, server, isReady); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/legacy/plugins/rollup/index.js b/x-pack/legacy/plugins/rollup/index.js index 3b6c033a2d85a..e0c00a7db62f0 100644 --- a/x-pack/legacy/plugins/rollup/index.js +++ b/x-pack/legacy/plugins/rollup/index.js @@ -57,12 +57,13 @@ export function rollup(kibana) { ], }, init: function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; registerLicenseChecker(server); registerIndicesRoute(server); registerFieldsForWildcardRoute(server); registerSearchRoute(server); registerJobsRoute(server); - registerRollupUsageCollector(server); + registerRollupUsageCollector(usageCollection, server); if ( server.plugins.index_management && server.plugins.index_management.addIndexManagementDataEnricher diff --git a/x-pack/legacy/plugins/rollup/server/usage/collector.js b/x-pack/legacy/plugins/rollup/server/usage/collector.js index 977253dfa53fb..99fffa774baaf 100644 --- a/x-pack/legacy/plugins/rollup/server/usage/collector.js +++ b/x-pack/legacy/plugins/rollup/server/usage/collector.js @@ -163,10 +163,10 @@ async function fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPa }; } -export function registerRollupUsageCollector(server) { +export function registerRollupUsageCollector(usageCollection, server) { const kibanaIndex = server.config().get('kibana.index'); - const collector = server.usage.collectorSet.makeUsageCollector({ + const collector = usageCollection.makeUsageCollector({ type: ROLLUP_USAGE_TYPE, isReady: () => true, fetch: async callCluster => { @@ -198,5 +198,5 @@ export function registerRollupUsageCollector(server) { }, }); - server.usage.collectorSet.register(collector); + usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 598d115a39e49..8f995d3c12c2a 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -126,7 +126,6 @@ export const spaces = (kibana: Record) => kibanaIndex: config.get('kibana.index'), }, savedObjects: server.savedObjects, - usage: server.usage, tutorial: { addScopedTutorialContextFactory: server.addScopedTutorialContextFactory, }, diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts index f1762498246c7..1be728d263372 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/index.ts @@ -43,11 +43,12 @@ export function upgradeAssistant(kibana: any) { init(server: Legacy.Server) { // Add server routes and initialize the plugin here const instance = plugin({} as any); + const { usageCollection } = server.newPlatform.setup.plugins; instance.setup(server.newPlatform.setup.core, { + usageCollection, __LEGACY: { // Legacy objects events: server.events, - usage: server.usage, savedObjects: server.savedObjects, // Legacy functions diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts index 4c378ba25430e..5f95f6e9fd555 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts @@ -15,12 +15,6 @@ import { upsertUIOpenOption } from './es_ui_open_apis'; describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { const mockIncrementCounter = jest.fn(); const server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { - makeUsageCollector: {}, - register: {}, - }, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts index 26302de74743f..3f2c80f7d6b75 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts @@ -15,12 +15,6 @@ import { upsertUIReindexOption } from './es_ui_reindex_apis'; describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { const mockIncrementCounter = jest.fn(); const server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { - makeUsageCollector: {}, - register: {}, - }, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts index 7d1d734748a82..898da4ab0073b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { makeUpgradeAssistantUsageCollector } from './usage_collector'; +export { registerUpgradeAssistantUsageCollector } from './usage_collector'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts index f0553578b86c8..27a0eef0d16f6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as usageCollector from './usage_collector'; +import { registerUpgradeAssistantUsageCollector } from './usage_collector'; /** * Since these route callbacks are so thin, these serve simply as integration tests @@ -16,15 +16,16 @@ describe('Upgrade Assistant Usage Collector', () => { let registerStub: any; let server: any; let callClusterStub: any; + let usageCollection: any; beforeEach(() => { makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, - register: {}, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { @@ -55,20 +56,20 @@ describe('Upgrade Assistant Usage Collector', () => { }); }); - describe('makeUpgradeAssistantUsageCollector', () => { - it('should call collectorSet.register', () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + describe('registerUpgradeAssistantUsageCollector', () => { + it('should registerCollector', () => { + registerUpgradeAssistantUsageCollector(usageCollection, server()); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = upgrade-assistant', () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + registerUpgradeAssistantUsageCollector(usageCollection, server()); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('upgrade-assistant-telemetry'); }); it('fetchUpgradeAssistantMetrics should return correct info', async () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + registerUpgradeAssistantUsageCollector(usageCollection, server()); const upgradeAssistantStats = await makeUsageCollectorStub.mock.calls[0][0].fetch( callClusterStub ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts index 47a2cd5d51fd4..99c0441063ce6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts @@ -7,6 +7,7 @@ import { set } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsRepository } from 'src/core/server/saved_objects/service/lib/repository'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE, @@ -97,12 +98,15 @@ export async function fetchUpgradeAssistantMetrics( }; } -export function makeUpgradeAssistantUsageCollector(server: ServerShim) { - const upgradeAssistantUsageCollector = server.usage.collectorSet.makeUsageCollector({ +export function registerUpgradeAssistantUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerShim +) { + const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ type: UPGRADE_ASSISTANT_TYPE, isReady: () => true, fetch: async (callCluster: any) => fetchUpgradeAssistantMetrics(callCluster, server), }); - server.usage.collectorSet.register(upgradeAssistantUsageCollector); + usageCollection.registerCollector(upgradeAssistantUsageCollector); } diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts index 7bc33142ca321..3d4247ffe70bb 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ServerShim, ServerShimWithRouter } from './types'; import { credentialStoreFactory } from './lib/reindexing/credential_store'; -import { makeUpgradeAssistantUsageCollector } from './lib/telemetry'; +import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; @@ -14,7 +15,10 @@ import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/re import { registerTelemetryRoutes } from './routes/telemetry'; export class UpgradeAssistantServerPlugin implements Plugin { - setup({ http }: CoreSetup, { __LEGACY }: { __LEGACY: ServerShim }) { + setup( + { http }: CoreSetup, + { __LEGACY, usageCollection }: { usageCollection: UsageCollectionSetup; __LEGACY: ServerShim } + ) { const router = http.createRouter(); const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router }; registerClusterCheckupRoutes(shimWithRouter); @@ -33,7 +37,7 @@ export class UpgradeAssistantServerPlugin implements Plugin const initializerContext = {} as PluginInitializerContext; const { savedObjects } = server; const { elasticsearch, xpack_main } = server.plugins; + const { usageCollection } = server.newPlatform.setup.plugins; + plugin(initializerContext).setup( { route: (arg: any) => server.route(arg), @@ -44,7 +46,7 @@ export const uptime = (kibana: any) => { elasticsearch, savedObjects, - usageCollector: server.usage, + usageCollection, xpack: xpack_main, } ); diff --git a/x-pack/legacy/plugins/uptime/server/kibana.index.ts b/x-pack/legacy/plugins/uptime/server/kibana.index.ts index 874fb2e37e902..73fabc629946b 100644 --- a/x-pack/legacy/plugins/uptime/server/kibana.index.ts +++ b/x-pack/legacy/plugins/uptime/server/kibana.index.ts @@ -22,17 +22,13 @@ export interface KibanaRouteOptions { export interface KibanaServer extends Server { route: (options: KibanaRouteOptions) => void; - usage: { - collectorSet: { - register: (usageCollector: any) => any; - }; - }; } export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => { - const { usageCollector, xpack } = plugins; + const { usageCollection, xpack } = plugins; const libs = compose(server, plugins); - usageCollector.collectorSet.register(KibanaTelemetryAdapter.initUsageCollector(usageCollector)); + KibanaTelemetryAdapter.registerUsageCollector(usageCollection); + initUptimeServer(libs); xpack.registerFeature({ diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index a31b4f99c522a..df2723283f88c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -9,6 +9,7 @@ import { GraphQLSchema } from 'graphql'; import { Lifecycle, ResponseToolkit } from 'hapi'; import { RouteOptions } from 'hapi'; import { SavedObjectsLegacyService } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; export interface UMFrameworkRequest { user: string; @@ -37,7 +38,7 @@ export interface UptimeCoreSetup { export interface UptimeCorePlugins { elasticsearch: any; savedObjects: SavedObjectsLegacyService; - usageCollector: any; + usageCollection: UsageCollectionSetup; xpack: any; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts index d8279cb3399bd..8e4011b4cf0eb 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts @@ -7,21 +7,19 @@ import { KibanaTelemetryAdapter } from '../kibana_telemetry_adapter'; describe('KibanaTelemetryAdapter', () => { - let telemetry: any; + let usageCollection: any; let collector: { type: string; fetch: () => Promise; isReady: () => boolean }; beforeEach(() => { - telemetry = { - collectorSet: { - makeUsageCollector: (val: any) => { - collector = val; - }, + usageCollection = { + makeUsageCollector: (val: any) => { + collector = val; }, }; }); it('collects monitor and overview data', async () => { expect.assertions(1); - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); KibanaTelemetryAdapter.countMonitor(); KibanaTelemetryAdapter.countOverview(); KibanaTelemetryAdapter.countOverview(); @@ -33,7 +31,7 @@ describe('KibanaTelemetryAdapter', () => { expect.assertions(1); // give a time of > 24 hours ago Date.now = jest.fn(() => 1559053560000); - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); KibanaTelemetryAdapter.countMonitor(); KibanaTelemetryAdapter.countOverview(); // give a time of now @@ -47,7 +45,7 @@ describe('KibanaTelemetryAdapter', () => { }); it('defaults ready to `true`', async () => { - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); expect(collector.isReady()).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index a906c741c5241..8dec0c1d2d485 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; interface UptimeTelemetry { overview_page: number; @@ -19,9 +20,13 @@ const BUCKET_SIZE = 3600; const BUCKET_NUMBER = 24; export class KibanaTelemetryAdapter { - public static initUsageCollector(usageCollector: any) { - const { collectorSet } = usageCollector; - return collectorSet.makeUsageCollector({ + public static registerUsageCollector = (usageCollector: UsageCollectionSetup) => { + const collector = KibanaTelemetryAdapter.initUsageCollector(usageCollector); + usageCollector.registerCollector(collector); + }; + + public static initUsageCollector(usageCollector: UsageCollectionSetup) { + return usageCollector.makeUsageCollector({ type: 'uptime', fetch: async () => { const report = this.getReport(); diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js index d5753ef6f3c85..021464f32a203 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js @@ -23,8 +23,8 @@ export function settingsRoute(server, kbnServer) { const callCluster = (...args) => callWithRequest(req, ...args); // All queries from HTTP API must use authentication headers from the request try { - const { collectorSet } = server.usage; - const settingsCollector = collectorSet.getCollectorByType(KIBANA_SETTINGS_TYPE); + const { usageCollection } = server.newPlatform.setup.plugins; + const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE); let settings = await settingsCollector.fetch(callCluster); if (!settings) { diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 313e4415a8e7c..d806aaf1807ef 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "spaces"], "requiredPlugins": ["features", "licensing"], - "optionalPlugins": ["security", "home"], + "optionalPlugins": ["security", "home", "usageCollection"], "server": true, "ui": false } diff --git a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts similarity index 83% rename from x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts rename to x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts index 912cccbc01782..b343bac9343a3 100644 --- a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSpacesUsageCollector, UsageStats } from './get_spaces_usage_collector'; +import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector'; import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; import { Feature } from '../../../features/server'; @@ -42,10 +42,8 @@ function setup({ return { licensing, features: featuresSetup, - usage: { - collectorSet: { - makeUsageCollector: (options: any) => new MockUsageCollector(options), - }, + usageCollecion: { + makeUsageCollector: (options: any) => new MockUsageCollector(options), }, }; } @@ -71,10 +69,11 @@ const defaultCallClusterMock = jest.fn().mockResolvedValue({ describe('with a basic license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usage } = setup({ license: { isAvailable: true, type: 'basic' } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ + const { features, licensing, usageCollecion } = setup({ + license: { isAvailable: true, type: 'basic' }, + }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { kibanaIndex: '.kibana', - usage, features, licensing, }); @@ -106,10 +105,9 @@ describe('with a basic license', () => { describe('with no license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usage } = setup({ license: { isAvailable: false } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ + const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { kibanaIndex: '.kibana', - usage, features, licensing, }); @@ -136,12 +134,11 @@ describe('with no license', () => { describe('with platinum license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usage } = setup({ + const { features, licensing, usageCollecion } = setup({ license: { isAvailable: true, type: 'platinum' }, }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { kibanaIndex: '.kibana', - usage, features, licensing, }); diff --git a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts similarity index 88% rename from x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts rename to x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts index bfbc5e6ab775d..eb6843cfe4538 100644 --- a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts @@ -7,6 +7,7 @@ import { get } from 'lodash'; import { CallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; // @ts-ignore import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; @@ -116,7 +117,6 @@ export interface UsageStats { interface CollectorDeps { kibanaIndex: string; - usage: { collectorSet: any }; features: PluginsSetup['features']; licensing: PluginsSetup['licensing']; } @@ -125,9 +125,11 @@ interface CollectorDeps { * @param {Object} server * @return {Object} kibana usage stats type collection object */ -export function getSpacesUsageCollector(deps: CollectorDeps) { - const { collectorSet } = deps.usage; - return collectorSet.makeUsageCollector({ +export function getSpacesUsageCollector( + usageCollection: UsageCollectionSetup, + deps: CollectorDeps +) { + return usageCollection.makeUsageCollector({ type: KIBANA_SPACES_STATS_TYPE, isReady: () => true, fetch: async (callCluster: CallCluster) => { @@ -165,3 +167,14 @@ export function getSpacesUsageCollector(deps: CollectorDeps) { }, }); } + +export function registerSpacesUsageCollector( + usageCollection: UsageCollectionSetup | undefined, + deps: CollectorDeps +) { + if (!usageCollection) { + return; + } + const collector = getSpacesUsageCollector(usageCollection, deps); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 6511a5dc3f31b..9d45dbb1b748d 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -7,6 +7,8 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { CapabilitiesModifier } from 'src/legacy/server/capabilities'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { HomeServerPluginSetup } from 'src/plugins/home/server'; import { SavedObjectsLegacyService, CoreSetup, @@ -24,25 +26,19 @@ import { AuditLogger } from '../../../../server/lib/audit_logger'; import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; -import { getSpacesUsageCollector } from './lib/get_spaces_usage_collector'; +import { registerSpacesUsageCollector } from './lib/spaces_usage_collector'; import { SpacesService } from './spaces_service'; import { SpacesServiceSetup } from './spaces_service/spaces_service'; import { ConfigType } from './config'; import { toggleUICapabilities } from './lib/toggle_ui_capabilities'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; import { initExternalSpacesApi } from './routes/api/external'; -import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin * to function properly. */ export interface LegacyAPI { savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - register: (collector: any) => void; - }; - }; tutorial: { addScopedTutorialContextFactory: (factory: any) => void; }; @@ -62,6 +58,7 @@ export interface PluginsSetup { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; security?: SecurityPluginSetup; + usageCollection?: UsageCollectionSetup; home?: HomeServerPluginSetup; } @@ -150,7 +147,12 @@ export class Plugin { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; - this.setupLegacyComponents(spacesService, plugins.features, plugins.licensing); + this.setupLegacyComponents( + spacesService, + plugins.features, + plugins.licensing, + plugins.usageCollection + ); }, createDefaultSpace: async () => { const esClient = await core.elasticsearch.adminClient$.pipe(take(1)).toPromise(); @@ -168,7 +170,8 @@ export class Plugin { private setupLegacyComponents( spacesService: SpacesServiceSetup, featuresSetup: FeaturesPluginSetup, - licensingSetup: LicensingPluginSetup + licensingSetup: LicensingPluginSetup, + usageCollectionSetup?: UsageCollectionSetup ) { const legacyAPI = this.getLegacyAPI(); const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects; @@ -180,6 +183,12 @@ export class Plugin { legacyAPI.tutorial.addScopedTutorialContextFactory( createSpacesTutorialContextFactory(spacesService) ); + // Register a function with server to manage the collection of usage stats + registerSpacesUsageCollector(usageCollectionSetup, { + kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, + features: featuresSetup, + licensing: licensingSetup, + }); legacyAPI.capabilities.registerCapabilitiesModifier(async (request, uiCapabilities) => { try { const activeSpace = await spacesService.getActiveSpace(KibanaRequest.from(request)); @@ -189,14 +198,5 @@ export class Plugin { return uiCapabilities; } }); - // Register a function with server to manage the collection of usage stats - legacyAPI.usage.collectorSet.register( - getSpacesUsageCollector({ - kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, - usage: legacyAPI.usage, - features: featuresSetup, - licensing: licensingSetup, - }) - ); } } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts index 38a973c1203d5..62820466b571c 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts @@ -106,7 +106,6 @@ export const createLegacyAPI = ({ auditLogger: {} as any, capabilities: {} as any, tutorial: {} as any, - usage: {} as any, xpackMain: {} as any, savedObjects: savedObjectsService, }; From ce166099a3d74427fa0a3bdb7c5983b3bef0f616 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 26 Nov 2019 19:44:46 -0500 Subject: [PATCH 60/63] [Uptime] Update snapshot counts (#48035) * Add snapshot count function that uses monitor iterator class. * Add js-doc comment. * Add snapshot count route. * Start adding snapshot state management. * Commit changes that were missed in previous. * Finish implementing snapshot count redux code. * Add basePath setter and type to ui state. * Dispatch basePath set action on app render. * Replace GQL-powered Snapshot export with Redux/Rest-powered version. * Extract presentational element to dedicated component. * Update broken test. * Rename action. * Add comments to clarify adapter function. * Remove obsolete code. * Add ui state field to store for tracking when app refreshes. * Make snapshot component refresh on global refresh via redux state. * Remove obsolete @ts-igore. * Alphabetize imports. * Port functional test fixture to REST test. * Delete snapshot GQL query. * Update API test fixtures to match new snapshot count REST response shape. * Update Snapshot count type check to be stricter. * Add tests for Snapshot API call. * Rename new test file from tsx to ts, it has no JSX. * Add tests for snapshot reducer. * Add tests for UI reducer. * Add tests for selectors. * Delete unused test file. * Move snapshot getter and map/reduce logic to dedicated helper function. * Add test for snapshot helper function. * Export type from module. * Rename outdated snapshot file. * Add action creator for fetch success. * Reorganize ui actions file. * Update snapshot effect to put error when input params are not valid. * Simplify typing code for a function. * Simplify snapshot count reduction. * Rename a function. * Rewrite a function to increase code clarity. * Remove duplicated interface. * Add very high ceiling for snapshot count iteration. * Update broken test assertion. --- .../common/constants/context_defaults.ts | 6 + .../uptime/common/runtime_types/index.ts | 1 + .../common/runtime_types/snapshot/index.ts | 7 + .../runtime_types/snapshot/snapshot_count.ts | 16 ++ .../functional/__tests__/snapshot.test.tsx | 18 +- .../public/components/functional/snapshot.tsx | 118 ++++++++++--- .../components/functional/status_panel.tsx | 16 +- .../functional/uptime_date_picker.tsx | 1 - .../plugins/uptime/public/pages/overview.tsx | 4 + .../plugins/uptime/public/queries/index.ts | 1 - .../uptime/public/queries/snapshot_query.ts | 34 ---- .../uptime/public/state/actions/index.ts | 1 + .../uptime/public/state/actions/snapshot.ts | 62 +++++++ .../plugins/uptime/public/state/actions/ui.ts | 24 ++- .../__snapshots__/snapshot.test.ts.snap | 8 + .../state/api/__tests__/snapshot.test.ts | 73 +++++++++ .../plugins/uptime/public/state/api/index.ts | 2 + .../uptime/public/state/api/snapshot.ts | 46 ++++++ .../uptime/public/state/effects/index.ts | 2 + .../uptime/public/state/effects/snapshot.ts | 45 +++++ .../plugins/uptime/public/state/index.ts | 4 +- .../__snapshots__/snapshot.test.ts.snap | 55 +++++++ .../__tests__/__snapshots__/ui.test.ts.snap | 28 ++++ .../state/reducers/__tests__/snapshot.test.ts | 64 ++++++++ .../state/reducers/__tests__/ui.test.ts | 55 +++++++ .../uptime/public/state/reducers/index.ts | 6 +- .../uptime/public/state/reducers/snapshot.ts | 53 ++++++ .../uptime/public/state/reducers/ui.ts | 8 + .../state/selectors/__tests__/index.test.ts | 42 +++++ .../uptime/public/state/selectors/index.ts | 6 +- .../plugins/uptime/public/uptime_app.tsx | 6 +- .../server/graphql/monitors/resolvers.ts | 27 --- .../server/graphql/monitors/schema.gql.ts | 18 -- .../get_snapshot_helper.test.ts.snap | 10 ++ .../__tests__/example_filter.json | 82 --------- .../__tests__/get_snapshot_helper.test.ts | 106 ++++++++++++ .../adapters/monitor_states/adapter_types.ts | 7 + .../elasticsearch_monitor_states_adapter.ts | 23 +++ .../monitor_states/get_snapshot_helper.ts | 40 +++++ .../adapters/monitor_states/search/index.ts | 1 + .../search/monitor_group_iterator.ts | 9 +- .../lib/adapters/monitors/adapter_types.ts | 7 - .../elasticsearch_monitors_adapter.ts | 155 +----------------- .../plugins/uptime/server/rest_api/index.ts | 8 +- .../rest_api/snapshot/get_snapshot_count.ts | 35 ++++ .../uptime/server/rest_api/snapshot/index.ts | 7 + .../uptime/graphql/fixtures/snapshot.json | 12 +- .../graphql/fixtures/snapshot_empty.json | 12 +- .../fixtures/snapshot_filtered_by_down.json | 12 +- .../fixtures/snapshot_filtered_by_up.json | 12 +- .../apis/uptime/graphql/index.js | 1 - .../apis/uptime/graphql/snapshot.js | 101 ------------ .../test/api_integration/apis/uptime/index.js | 1 + .../api_integration/apis/uptime/rest/index.ts | 16 ++ .../apis/uptime/rest/snapshot.ts | 52 ++++++ 55 files changed, 1057 insertions(+), 509 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts create mode 100644 x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts create mode 100644 x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts create mode 100644 x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts delete mode 100644 x-pack/test/api_integration/apis/uptime/graphql/snapshot.js create mode 100644 x-pack/test/api_integration/apis/uptime/rest/index.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts index 3a42df8c5e9ab..4c32769d73e84 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts @@ -35,4 +35,10 @@ export const CONTEXT_DEFAULTS = { cursorDirection: CursorDirection.AFTER, sortOrder: SortOrder.ASC, }, + + /** + * Defines the maximum number of monitors to iterate on + * in a single count session. The intention is to catch as many as possible. + */ + MAX_MONITORS_FOR_SNAPSHOT_COUNT: 1000000, }; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts index 3a5d0549c5d45..a88e28f2e5a09 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './snapshot'; export * from './monitor/monitor_details'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts new file mode 100644 index 0000000000000..99bf783d3ab2e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Snapshot, SnapshotType } from './snapshot_count'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts new file mode 100644 index 0000000000000..d4935c50ff5b8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.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 * as t from 'io-ts'; + +export const SnapshotType = t.type({ + down: t.number, + mixed: t.number, + total: t.number, + up: t.number, +}); + +export type Snapshot = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index 40e3daae67185..193f37c8fe56b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -6,21 +6,19 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Snapshot as SnapshotType } from '../../../../common/graphql/types'; -import { SnapshotComponent } from '../snapshot'; +import { Snapshot } from '../../../../common/runtime_types'; +import { PresentationalComponent } from '../snapshot'; describe('Snapshot component', () => { - const snapshot: SnapshotType = { - counts: { - up: 8, - down: 2, - mixed: 0, - total: 10, - }, + const snapshot: Snapshot = { + up: 8, + down: 2, + mixed: 0, + total: 10, }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index ddc6df14c2ade..e0d282a5348a0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,46 +5,69 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect } from 'react'; import { get } from 'lodash'; +import { connect } from 'react-redux'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { Snapshot as SnapshotType } from '../../../common/graphql/types'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; -import { snapshotQuery } from '../../queries'; +import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; +import { AppState } from '../../state'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -interface SnapshotQueryResult { - snapshot?: SnapshotType; -} - -interface SnapshotProps { +/** + * Props expected from parent components. + */ +interface OwnProps { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; /** * Height is needed, since by default charts takes height of 100% */ height?: string; + statusFilter?: string; } -export type SnapshotComponentProps = SnapshotProps & UptimeGraphQLQueryProps; +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} /** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component + * Contains functions that will dispatch actions used + * for this component's lifecyclel + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. */ -export const SnapshotComponent = ({ data, loading, height }: SnapshotComponentProps) => ( +type Props = OwnProps & StoreProps & DispatchProps; + +type PresentationalComponentProps = Pick & + Pick; + +export const PresentationalComponent: React.FC = ({ + count, + height, + loading, +}) => ( - (data, 'snapshot.counts.down', 0)} - total={get(data, 'snapshot.counts.total', 0)} - /> + (count, 'down', 0)} total={get(count, 'total', 0)} /> (data, 'snapshot.counts.up', 0)} - down={get(data, 'snapshot.counts.down', 0)} + up={get(count, 'up', 0)} + down={get(count, 'down', 0)} height={SNAPSHOT_CHART_HEIGHT} width={SNAPSHOT_CHART_WIDTH} /> @@ -54,8 +77,55 @@ export const SnapshotComponent = ({ data, loading, height }: SnapshotComponentPr /** * This component visualizes a KPI and histogram chart to help users quickly * glean the status of their uptime environment. + * @param props the props required by the component */ -export const Snapshot = withUptimeGraphQL( - SnapshotComponent, - snapshotQuery -); +export const Container: React.FC = ({ + count, + dateRangeStart, + dateRangeEnd, + filters, + height, + statusFilter, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, statusFilter]); + return ; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index a58d06ece0ede..b74bc943dc3eb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -12,6 +12,10 @@ import { Snapshot } from './snapshot'; interface StatusPanelProps { absoluteDateRangeStart: number; absoluteDateRangeEnd: number; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; sharedProps: { [key: string]: any }; } @@ -20,12 +24,22 @@ const STATUS_CHART_HEIGHT = '160px'; export const StatusPanel = ({ absoluteDateRangeStart, absoluteDateRangeEnd, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, sharedProps, }: StatusPanelProps) => ( - + { updateUrl({ dateRangeStart: start, dateRangeEnd: end }); refreshApp(); }} - // @ts-ignore onRefresh is not defined on EuiSuperDatePicker's type yet onRefresh={refreshApp} onRefreshChange={({ isPaused, refreshInterval }: SuperDateRangePickerRefreshChangedEvent) => { updateUrl({ diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 09d40d32b696c..561cc934a9b76 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -150,6 +150,10 @@ export const OverviewPage = ({ diff --git a/x-pack/legacy/plugins/uptime/public/queries/index.ts b/x-pack/legacy/plugins/uptime/public/queries/index.ts index d680ec6c543c4..b86522c03aba8 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/index.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/index.ts @@ -10,4 +10,3 @@ export { monitorChartsQuery, monitorChartsQueryString } from './monitor_charts_q export { monitorPageTitleQuery } from './monitor_page_title_query'; export { monitorStatusBarQuery, monitorStatusBarQueryString } from './monitor_status_bar_query'; export { pingsQuery, pingsQueryString } from './pings_query'; -export { snapshotQuery, snapshotQueryString } from './snapshot_query'; diff --git a/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts b/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts deleted file mode 100644 index 2db226876d220..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts +++ /dev/null @@ -1,34 +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 gql from 'graphql-tag'; - -export const snapshotQueryString = ` -query Snapshot( - $dateRangeStart: String! - $dateRangeEnd: String! - $filters: String - $statusFilter: String -) { - snapshot: getSnapshot( - dateRangeStart: $dateRangeStart - dateRangeEnd: $dateRangeEnd - filters: $filters - statusFilter: $statusFilter - ) { - counts { - down - mixed - up - total - } - } -} -`; - -export const snapshotQuery = gql` - ${snapshotQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts index 1a33812ca8566..6b896b07bb066 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './snapshot'; export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts new file mode 100644 index 0000000000000..fe87a6a5960ee --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.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 { Snapshot } from '../../../common/runtime_types'; +export const FETCH_SNAPSHOT_COUNT = 'FETCH_SNAPSHOT_COUNT'; +export const FETCH_SNAPSHOT_COUNT_FAIL = 'FETCH_SNAPSHOT_COUNT_FAIL'; +export const FETCH_SNAPSHOT_COUNT_SUCCESS = 'FETCH_SNAPSHOT_COUNT_SUCCESS'; + +export interface GetSnapshotPayload { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; +} + +interface GetSnapshotCountFetchAction { + type: typeof FETCH_SNAPSHOT_COUNT; + payload: GetSnapshotPayload; +} + +interface GetSnapshotCountSuccessAction { + type: typeof FETCH_SNAPSHOT_COUNT_SUCCESS; + payload: Snapshot; +} + +interface GetSnapshotCountFailAction { + type: typeof FETCH_SNAPSHOT_COUNT_FAIL; + payload: Error; +} + +export type SnapshotActionTypes = + | GetSnapshotCountFetchAction + | GetSnapshotCountSuccessAction + | GetSnapshotCountFailAction; + +export const fetchSnapshotCount = ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string +): GetSnapshotCountFetchAction => ({ + type: FETCH_SNAPSHOT_COUNT, + payload: { + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }, +}); + +export const fetchSnapshotCountFail = (error: Error): GetSnapshotCountFailAction => ({ + type: FETCH_SNAPSHOT_COUNT_FAIL, + payload: error, +}); + +export const fetchSnapshotCountSuccess = (snapshot: Snapshot) => ({ + type: FETCH_SNAPSHOT_COUNT_SUCCESS, + payload: snapshot, +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts index f0234f903d3d8..0bb2d8447419b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts @@ -6,22 +6,33 @@ export const SET_INTEGRATION_POPOVER_STATE = 'SET_INTEGRATION_POPOVER_STATE'; export const SET_BASE_PATH = 'SET_BASE_PATH'; +export const REFRESH_APP = 'REFRESH_APP'; export interface PopoverState { id: string; open: boolean; } +interface SetBasePathAction { + type: typeof SET_BASE_PATH; + payload: string; +} + interface SetIntegrationPopoverAction { type: typeof SET_INTEGRATION_POPOVER_STATE; payload: PopoverState; } -interface SetBasePathAction { - type: typeof SET_BASE_PATH; - payload: string; +interface TriggerAppRefreshAction { + type: typeof REFRESH_APP; + payload: number; } +export type UiActionTypes = + | SetIntegrationPopoverAction + | SetBasePathAction + | TriggerAppRefreshAction; + export function toggleIntegrationsPopover(popoverState: PopoverState): SetIntegrationPopoverAction { return { type: SET_INTEGRATION_POPOVER_STATE, @@ -36,4 +47,9 @@ export function setBasePath(basePath: string): SetBasePathAction { }; } -export type UiActionTypes = SetIntegrationPopoverAction | SetBasePathAction; +export function triggerAppRefresh(refreshTime: number): TriggerAppRefreshAction { + return { + type: REFRESH_APP, + payload: refreshTime, + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000000..53716681664c2 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot API throws when server response doesn't correspond to expected type 1`] = ` +[Error: Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/down: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/mixed: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/total: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/up: number] +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts new file mode 100644 index 0000000000000..f5fdfb172bc58 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchSnapshotCount } from '../snapshot'; + +describe('snapshot API', () => { + let fetchMock: jest.SpyInstance>>; + let mockResponse: Partial; + + beforeEach(() => { + fetchMock = jest.spyOn(window, 'fetch'); + mockResponse = { + ok: true, + json: () => new Promise(r => r({ up: 3, down: 12, mixed: 0, total: 15 })), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls url with expected params and returns response body on 200', async () => { + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + const resp = await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id:"auto-http-0X21EE76EAC459873F"', + statusFilter: 'up', + }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/uptime/snapshot/count?dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%22auto-http-0X21EE76EAC459873F%22&statusFilter=up' + ); + expect(resp).toEqual({ up: 3, down: 12, mixed: 0, total: 15 }); + }); + + it(`throws when server response doesn't correspond to expected type`, async () => { + mockResponse = { ok: true, json: () => new Promise(r => r({ foo: 'bar' })) }; + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + let error: Error | undefined; + try { + await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id: baz', + statusFilter: 'up', + }); + } catch (e) { + error = e; + } + expect(error).toMatchSnapshot(); + }); + + it('throws an error when response is not ok', async () => { + mockResponse = { ok: false, statusText: 'There was an error fetching your data.' }; + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + let error: Error | undefined; + try { + await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }); + } catch (e) { + error = e; + } + expect(error).toEqual(new Error('There was an error fetching your data.')); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts index e9b8082b417ba..a4429868494f1 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -3,4 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + export * from './monitor'; +export * from './snapshot'; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts new file mode 100644 index 0000000000000..cbfe00a4a8746 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { getApiPath } from '../../lib/helper'; +import { SnapshotType, Snapshot } from '../../../common/runtime_types'; + +interface ApiRequest { + basePath: string; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; +} + +export const fetchSnapshotCount = async ({ + basePath, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, +}: ApiRequest): Promise => { + const url = getApiPath(`/api/uptime/snapshot/count`, basePath); + const params = { + dateRangeStart, + dateRangeEnd, + ...(filters && { filters }), + ...(statusFilter && { statusFilter }), + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + const decoded = SnapshotType.decode(responseData); + ThrowReporter.report(decoded); + if (isRight(decoded)) { + return decoded.right; + } + throw new Error('`getSnapshotCount` response did not correspond to expected type'); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts index 92802f2e0c84a..4eb027d642974 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -6,7 +6,9 @@ import { fork } from 'redux-saga/effects'; import { fetchMonitorDetailsEffect } from './monitor'; +import { fetchSnapshotCountSaga } from './snapshot'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); + yield fork(fetchSnapshotCountSaga); } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts new file mode 100644 index 0000000000000..23ac1016d2244 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.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 { call, put, takeLatest, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { + FETCH_SNAPSHOT_COUNT, + GetSnapshotPayload, + fetchSnapshotCountFail, + fetchSnapshotCountSuccess, +} from '../actions'; +import { fetchSnapshotCount } from '../api'; +import { getBasePath } from '../selectors'; + +function* snapshotSaga(action: Action) { + try { + if (!action.payload) { + yield put( + fetchSnapshotCountFail(new Error('Cannot fetch snapshot for undefined parameters.')) + ); + return; + } + const { + payload: { dateRangeStart, dateRangeEnd, filters, statusFilter }, + } = action; + const basePath = yield select(getBasePath); + const response = yield call(fetchSnapshotCount, { + basePath, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }); + yield put(fetchSnapshotCountSuccess(response)); + } catch (error) { + yield put(fetchSnapshotCountFail(error)); + } +} + +export function* fetchSnapshotCountSaga() { + yield takeLatest(FETCH_SNAPSHOT_COUNT, snapshotSaga); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/index.ts b/x-pack/legacy/plugins/uptime/public/state/index.ts index 01cffb636d33c..e3563c74294d2 100644 --- a/x-pack/legacy/plugins/uptime/public/state/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/index.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { compose, createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; - -import { rootReducer } from './reducers'; import { rootEffect } from './effects'; +import { rootReducer } from './reducers'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000000..d3a21ec9eece3 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot reducer appends a current error to existing errors list 1`] = ` +Object { + "count": Object { + "down": 0, + "mixed": 0, + "total": 0, + "up": 0, + }, + "errors": Array [ + [Error: I couldn't get your data because the server denied the request], + ], + "loading": false, +} +`; + +exports[`snapshot reducer changes the count when a snapshot fetch succeeds 1`] = ` +Object { + "count": Object { + "down": 15, + "mixed": 0, + "total": 25, + "up": 10, + }, + "errors": Array [], + "loading": false, +} +`; + +exports[`snapshot reducer sets the state's status to loading during a fetch 1`] = ` +Object { + "count": Object { + "down": 0, + "mixed": 0, + "total": 0, + "up": 0, + }, + "errors": Array [], + "loading": true, +} +`; + +exports[`snapshot reducer updates existing state 1`] = ` +Object { + "count": Object { + "down": 1, + "mixed": 0, + "total": 4, + "up": 3, + }, + "errors": Array [], + "loading": true, +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap new file mode 100644 index 0000000000000..75516da18c633 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ui reducer adds integration popover status to state 1`] = ` +Object { + "basePath": "", + "integrationsPopoverOpen": Object { + "id": "popover-2", + "open": true, + }, + "lastRefresh": 125, +} +`; + +exports[`ui reducer sets the application's base path 1`] = ` +Object { + "basePath": "yyz", + "integrationsPopoverOpen": null, + "lastRefresh": 125, +} +`; + +exports[`ui reducer updates the refresh value 1`] = ` +Object { + "basePath": "", + "integrationsPopoverOpen": null, + "lastRefresh": 125, +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts new file mode 100644 index 0000000000000..a4b317d5af197 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { snapshotReducer } from '../snapshot'; +import { SnapshotActionTypes } from '../../actions'; + +describe('snapshot reducer', () => { + it('updates existing state', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT', + payload: { + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'foo: bar', + statusFilter: 'up', + }, + }; + expect( + snapshotReducer( + { + count: { down: 1, mixed: 0, total: 4, up: 3 }, + errors: [], + loading: false, + }, + action + ) + ).toMatchSnapshot(); + }); + + it(`sets the state's status to loading during a fetch`, () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT', + payload: { + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }, + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); + + it('changes the count when a snapshot fetch succeeds', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT_SUCCESS', + payload: { + up: 10, + down: 15, + mixed: 0, + total: 25, + }, + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); + + it('appends a current error to existing errors list', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT_FAIL', + payload: new Error(`I couldn't get your data because the server denied the request`), + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts new file mode 100644 index 0000000000000..9be863f0b700d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionTypes } from '../../actions'; +import { uiReducer } from '../ui'; + +describe('ui reducer', () => { + it(`sets the application's base path`, () => { + const action: UiActionTypes = { + type: 'SET_BASE_PATH', + payload: 'yyz', + }; + expect( + uiReducer( + { + basePath: 'abc', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); + }); + + it('adds integration popover status to state', () => { + const action: UiActionTypes = { + type: 'SET_INTEGRATION_POPOVER_STATE', + payload: { + id: 'popover-2', + open: true, + }, + }; + expect( + uiReducer( + { + basePath: '', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); + }); + + it('updates the refresh value', () => { + const action: UiActionTypes = { + type: 'REFRESH_APP', + payload: 125, + }; + expect(uiReducer(undefined, action)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index 186b02395b779..f0c3d1c2cbecf 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -5,10 +5,12 @@ */ import { combineReducers } from 'redux'; -import { uiReducer } from './ui'; import { monitorReducer } from './monitor'; +import { snapshotReducer } from './snapshot'; +import { uiReducer } from './ui'; export const rootReducer = combineReducers({ - ui: uiReducer, monitor: monitorReducer, + snapshot: snapshotReducer, + ui: uiReducer, }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts new file mode 100644 index 0000000000000..dd9449325f4fb --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Snapshot } from '../../../common/runtime_types'; +import { + FETCH_SNAPSHOT_COUNT, + FETCH_SNAPSHOT_COUNT_FAIL, + FETCH_SNAPSHOT_COUNT_SUCCESS, + SnapshotActionTypes, +} from '../actions'; + +export interface SnapshotState { + count: Snapshot; + errors: any[]; + loading: boolean; +} + +const initialState: SnapshotState = { + count: { + down: 0, + mixed: 0, + total: 0, + up: 0, + }, + errors: [], + loading: false, +}; + +export function snapshotReducer(state = initialState, action: SnapshotActionTypes): SnapshotState { + switch (action.type) { + case FETCH_SNAPSHOT_COUNT: + return { + ...state, + loading: true, + }; + case FETCH_SNAPSHOT_COUNT_SUCCESS: + return { + ...state, + count: action.payload, + loading: false, + }; + case FETCH_SNAPSHOT_COUNT_FAIL: + return { + ...state, + errors: [...state.errors, action.payload], + }; + default: + return state; + } +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts index d095d6ba961ca..be95c8fff6bec 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts @@ -9,20 +9,28 @@ import { PopoverState, SET_INTEGRATION_POPOVER_STATE, SET_BASE_PATH, + REFRESH_APP, } from '../actions/ui'; export interface UiState { integrationsPopoverOpen: PopoverState | null; basePath: string; + lastRefresh: number; } const initialState: UiState = { integrationsPopoverOpen: null, basePath: '', + lastRefresh: Date.now(), }; export function uiReducer(state = initialState, action: UiActionTypes): UiState { switch (action.type) { + case REFRESH_APP: + return { + ...state, + lastRefresh: action.payload, + }; case SET_INTEGRATION_POPOVER_STATE: const popoverState = action.payload; return { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts new file mode 100644 index 0000000000000..70cd2b19860ba --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { getBasePath, isIntegrationsPopupOpen } from '../index'; +import { AppState } from '../../../state'; + +describe('state selectors', () => { + const state: AppState = { + monitor: { + monitorDetailsList: [], + loading: false, + errors: [], + }, + snapshot: { + count: { + up: 2, + down: 0, + mixed: 0, + total: 2, + }, + errors: [], + loading: false, + }, + ui: { basePath: 'yyz', integrationsPopoverOpen: null, lastRefresh: 125 }, + }; + + it('selects base path from state', () => { + expect(getBasePath(state)).toBe('yyz'); + }); + + it('gets integrations popup state', () => { + const integrationsPopupOpen = { + id: 'popup-id', + open: true, + }; + state.ui.integrationsPopoverOpen = integrationsPopupOpen; + expect(isIntegrationsPopupOpen(state)).toBe(integrationsPopupOpen); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 59c3f0c31539f..245b45a939950 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { AppState } from '../../state'; -export const isIntegrationsPopupOpen = (state: AppState) => state.ui.integrationsPopoverOpen; +export const getBasePath = ({ ui: { basePath } }: AppState) => basePath; -export const getBasePath = (state: AppState) => state.ui.basePath; +export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: AppState) => + integrationsPopoverOpen; export const getMonitorDetails = (state: AppState, summary: any) => { return state.monitor.monitorDetailsList[summary.monitor_id]; diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 3fdceb8b1b2bd..47743729c1e76 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -22,7 +22,7 @@ import { UptimeDatePicker } from './components/functional/uptime_date_picker'; import { useUrlParams } from './hooks'; import { getTitle } from './lib/helper/get_title'; import { store } from './state'; -import { setBasePath } from './state/actions'; +import { setBasePath, triggerAppRefresh } from './state/actions'; export interface UptimeAppColors { danger: string; @@ -116,7 +116,9 @@ const Application = (props: UptimeAppProps) => { }, []); const refreshApp = () => { - setLastRefresh(Date.now()); + const refreshTime = Date.now(); + setLastRefresh(refreshTime); + store.dispatch(triggerAppRefresh(refreshTime)); }; const [getUrlParams] = useUrlParams(); diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts index 96a386b6a6848..415afc87e201e 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -12,24 +12,15 @@ import { GetLatestMonitorsQueryArgs, GetMonitorChartsDataQueryArgs, GetMonitorPageTitleQueryArgs, - GetSnapshotQueryArgs, MonitorChart, MonitorPageTitle, Ping, - Snapshot, GetSnapshotHistogramQueryArgs, } from '../../../common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { CreateUMGraphQLResolvers, UMContext } from '../types'; import { HistogramResult } from '../../../common/domain_types'; -export type UMSnapshotResolver = UMResolver< - Snapshot | Promise, - any, - GetSnapshotQueryArgs, - UMContext ->; - export type UMMonitorsResolver = UMResolver, any, UMGqlRange, UMContext>; export type UMLatestMonitorsResolver = UMResolver< @@ -71,7 +62,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( libs: UMServerLibs ): { Query: { - getSnapshot: UMSnapshotResolver; getSnapshotHistogram: UMGetSnapshotHistogram; getMonitorChartsData: UMGetMonitorChartsResolver; getLatestMonitors: UMLatestMonitorsResolver; @@ -80,23 +70,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( }; } => ({ Query: { - async getSnapshot( - resolver, - { dateRangeStart, dateRangeEnd, filters, statusFilter }, - { req } - ): Promise { - const counts = await libs.monitors.getSnapshotCount( - req, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter - ); - - return { - counts, - }; - }, async getSnapshotHistogram( resolver, { dateRangeStart, dateRangeEnd, filters, monitorId, statusFilter }, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts index 97dcbd12fff2e..f9b14c63e70bb 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts @@ -31,17 +31,6 @@ export const monitorsSchema = gql` y: UnsignedInteger } - type SnapshotCount { - up: Int! - down: Int! - mixed: Int! - total: Int! - } - - type Snapshot { - counts: SnapshotCount! - } - type DataPoint { x: UnsignedInteger y: Float @@ -139,13 +128,6 @@ export const monitorsSchema = gql` statusFilter: String ): LatestMonitorsResult - getSnapshot( - dateRangeStart: String! - dateRangeEnd: String! - filters: String - statusFilter: String - ): Snapshot - getSnapshotHistogram( dateRangeStart: String! dateRangeEnd: String! diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap new file mode 100644 index 0000000000000..29c82ff455d36 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`get snapshot helper reduces check groups as expected 1`] = ` +Object { + "down": 1, + "mixed": 0, + "total": 3, + "up": 2, +} +`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json deleted file mode 100644 index bd4248755095d..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "bool": { - "filter": [ - { - "bool": { - "filter": [ - { - "bool": { - "should": [{ "match_phrase": { "monitor.id": "green-0001" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [{ "match_phrase": { "monitor.name": "" } }], - "minimum_should_match": 1 - } - } - ] - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0000" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0001" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0002" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0003" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0004" } }], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ] - } -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts new file mode 100644 index 0000000000000..917e4a149de67 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { getSnapshotCountHelper } from '../get_snapshot_helper'; +import { MonitorGroups } from '../search'; + +describe('get snapshot helper', () => { + let mockIterator: any; + beforeAll(() => { + mockIterator = jest.fn(); + const summaryTimestamp = new Date('2019-01-01'); + const firstResult: MonitorGroups = { + id: 'firstGroup', + groups: [ + { + monitorId: 'first-monitor', + location: 'us-east-1', + checkGroup: 'abc', + status: 'down', + summaryTimestamp, + }, + { + monitorId: 'first-monitor', + location: 'us-west-1', + checkGroup: 'abc', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'first-monitor', + location: 'amsterdam', + checkGroup: 'abc', + status: 'down', + summaryTimestamp, + }, + ], + }; + const secondResult: MonitorGroups = { + id: 'secondGroup', + groups: [ + { + monitorId: 'second-monitor', + location: 'us-east-1', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'second-monitor', + location: 'us-west-1', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'second-monitor', + location: 'amsterdam', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + ], + }; + const thirdResult: MonitorGroups = { + id: 'thirdGroup', + groups: [ + { + monitorId: 'third-monitor', + location: 'us-east-1', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'third-monitor', + location: 'us-west-1', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'third-monitor', + location: 'amsterdam', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + ], + }; + + const mockNext = jest + .fn() + .mockReturnValueOnce(firstResult) + .mockReturnValueOnce(secondResult) + .mockReturnValueOnce(thirdResult) + .mockReturnValueOnce(null); + mockIterator.next = mockNext; + }); + + it('reduces check groups as expected', async () => { + expect(await getSnapshotCountHelper(mockIterator)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts index 781f30314d350..57b1744f5d324 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts @@ -21,6 +21,13 @@ export interface UMMonitorStatesAdapter { statusFilter?: string | null ): Promise; statesIndexExists(request: any): Promise; + getSnapshotCount( + request: any, + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): Promise; } export interface CursorPagination { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts index 59c3e022e7d04..c3593854fa53f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts @@ -9,6 +9,9 @@ import { UMMonitorStatesAdapter, GetMonitorStatesResult, CursorPagination } from import { StatesIndexStatus } from '../../../../common/graphql/types'; import { INDEX_NAMES, CONTEXT_DEFAULTS } from '../../../../common/constants'; import { fetchPage } from './search'; +import { MonitorGroupIterator } from './search/monitor_group_iterator'; +import { Snapshot } from '../../../../common/runtime_types'; +import { getSnapshotCountHelper } from './get_snapshot_helper'; export interface QueryContext { database: any; @@ -57,6 +60,26 @@ export class ElasticsearchMonitorStatesAdapter implements UMMonitorStatesAdapter }; } + public async getSnapshotCount( + request: any, + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): Promise { + const context: QueryContext = { + database: this.database, + request, + dateRangeStart, + dateRangeEnd, + pagination: CONTEXT_DEFAULTS.CURSOR_PAGINATION, + filterClause: filters && filters !== '' ? JSON.parse(filters) : null, + size: CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT, + statusFilter, + }; + return getSnapshotCountHelper(new MonitorGroupIterator(context)); + } + public async statesIndexExists(request: any): Promise { // TODO: adapt this to the states index in future release const { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts new file mode 100644 index 0000000000000..8bd21b77406df --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.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 { MonitorGroups, MonitorGroupIterator } from './search'; +import { Snapshot } from '../../../../common/runtime_types'; + +const reduceItemsToCounts = (items: MonitorGroups[]) => { + let down = 0; + let up = 0; + items.forEach(item => { + if (item.groups.some(group => group.status === 'down')) { + down++; + } else { + up++; + } + }); + return { + down, + mixed: 0, + total: down + up, + up, + }; +}; + +export const getSnapshotCountHelper = async (iterator: MonitorGroupIterator): Promise => { + const items: MonitorGroups[] = []; + let res: MonitorGroups | null; + // query the index to find the most recent check group for each monitor/location + do { + res = await iterator.next(); + if (res) { + items.push(res); + } + } while (res !== null); + + return reduceItemsToCounts(items); +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts index 2fa2112161dcd..040c256935692 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts @@ -5,3 +5,4 @@ */ export { fetchPage, MonitorGroups, MonitorLocCheckGroup, MonitorGroupsPage } from './fetch_page'; +export { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts index 2fec58593e5d8..1de2dbb0e364d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts @@ -97,9 +97,12 @@ export class MonitorGroupIterator { } } - // Attempts to buffer more results fetching a single chunk. - // If trim is set to true, which is the default, it will delete all items in the buffer prior to the current item. - // to free up space. + /** + * Attempts to buffer more results fetching a single chunk. + * If trim is set to true, which is the default, it will delete all items in the buffer prior to the current item. + * to free up space. + * @param size the number of items to chunk + */ async attemptBufferMore( size: number = CHUNK_SIZE ): Promise<{ hasMore: boolean; gotHit: boolean }> { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts index 996e80d2c8613..f6ac587b0ceec 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts @@ -14,13 +14,6 @@ export interface UMMonitorsAdapter { dateRangeEnd: string, location?: string | null ): Promise; - getSnapshotCount( - request: any, - dateRangeStart: string, - dateRangeEnd: string, - filters?: string | null, - statusFilter?: string | null - ): Promise; getFilterBar(request: any, dateRangeStart: string, dateRangeEnd: string): Promise; getMonitorPageTitle(request: any, monitorId: string): Promise; getMonitorDetails(request: any, monitorId: string): Promise; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index 1a391e90f2a5e..ef0837a043172 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set, reduce } from 'lodash'; -import { INDEX_NAMES, QUERY } from '../../../../common/constants'; +import { get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; import { FilterBar, MonitorChart, @@ -13,7 +13,7 @@ import { Ping, LocationDurationLine, } from '../../../../common/graphql/types'; -import { getFilterClause, parseFilterQuery, getHistogramIntervalFormatted } from '../../helper'; +import { getHistogramIntervalFormatted } from '../../helper'; import { DatabaseAdapter } from '../database'; import { UMMonitorsAdapter } from './adapter_types'; import { MonitorDetails, Error } from '../../../../common/runtime_types'; @@ -184,155 +184,6 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { return monitorChartsData; } - /** - * Provides a count of the current monitors - * @param request Kibana request - * @param dateRangeStart timestamp bounds - * @param dateRangeEnd timestamp bounds - * @param filters filters defined by client - */ - public async getSnapshotCount( - request: any, - dateRangeStart: string, - dateRangeEnd: string, - filters?: string | null, - statusFilter?: string | null - ): Promise { - const query = parseFilterQuery(filters); - const additionalFilters = [{ exists: { field: 'summary.up' } }]; - if (query) { - additionalFilters.push(query); - } - const filter = getFilterClause(dateRangeStart, dateRangeEnd, additionalFilters); - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - bool: { - filter, - }, - }, - size: 0, - aggs: { - ids: { - composite: { - sources: [ - { - id: { - terms: { - field: 'monitor.id', - }, - }, - }, - { - location: { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - }, - ], - size: QUERY.DEFAULT_AGGS_CAP, - }, - aggs: { - latest: { - top_hits: { - sort: [{ '@timestamp': { order: 'desc' } }], - _source: { - includes: ['summary.*', 'monitor.id', '@timestamp', 'observer.geo.name'], - }, - size: 1, - }, - }, - }, - }, - }, - }, - }; - - let searchAfter: any = null; - - const summaryByIdLocation: { - // ID - [key: string]: { - // Location - [key: string]: { up: number; down: number; timestamp: number }; - }; - } = {}; - - do { - if (searchAfter) { - set(params, 'body.aggs.ids.composite.after', searchAfter); - } - - const queryResult = await this.database.search(request, params); - const idBuckets = get(queryResult, 'aggregations.ids.buckets', []); - - idBuckets.forEach(bucket => { - // We only get the latest doc - const source: any = get(bucket, 'latest.hits.hits[0]._source'); - const { - summary: { up, down }, - monitor: { id }, - } = source; - const timestamp = get(source, '@timestamp', 0); - const location = get(source, 'observer.geo.name', ''); - - let idSummary = summaryByIdLocation[id]; - if (!idSummary) { - idSummary = {}; - summaryByIdLocation[id] = idSummary; - } - const locationSummary = idSummary[location]; - if (!locationSummary || locationSummary.timestamp < timestamp) { - idSummary[location] = { timestamp, up, down }; - } - }); - - searchAfter = get(queryResult, 'aggregations.ids.after_key'); - } while (searchAfter); - - let up: number = 0; - let mixed: number = 0; - let down: number = 0; - - for (const id in summaryByIdLocation) { - if (!summaryByIdLocation.hasOwnProperty(id)) { - continue; - } - const locationInfo = summaryByIdLocation[id]; - const { up: locationUp, down: locationDown } = reduce( - locationInfo, - (acc, value, key) => { - acc.up += value.up; - acc.down += value.down; - return acc; - }, - { up: 0, down: 0 } - ); - - if (locationDown === 0) { - up++; - } else if (locationUp > 0) { - mixed++; - } else { - down++; - } - } - - const result: any = { up, down, mixed, total: up + down + mixed }; - if (statusFilter) { - for (const status in result) { - if (status !== 'total' && status !== statusFilter) { - result[status] = 0; - } - } - } - - return result; - } - /** * Fetch options for the filter bar. * @param request Kibana request object diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index cc702362a57a8..889f8a820b2f3 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -8,16 +8,18 @@ import { createIsValidRoute } from './auth'; import { createGetAllRoute } from './pings'; import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; +import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteCreator } from './types'; import { createGetMonitorDetailsRoute } from './monitors'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export const restApiRoutes: UMRestApiRouteCreator[] = [ - createIsValidRoute, createGetAllRoute, - createLogMonitorPageRoute, - createLogOverviewPageRoute, createGetIndexPatternRoute, createGetMonitorDetailsRoute, + createGetSnapshotCount, + createIsValidRoute, + createLogMonitorPageRoute, + createLogOverviewPageRoute, ]; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts new file mode 100644 index 0000000000000..ddca622005d63 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.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 Joi from 'joi'; +import { UMServerLibs } from '../../lib/lib'; +import { Snapshot } from '../../../common/runtime_types'; + +export const createGetSnapshotCount = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/snapshot/count', + options: { + validate: { + query: Joi.object({ + dateRangeStart: Joi.string().required(), + dateRangeEnd: Joi.string().required(), + filters: Joi.string(), + statusFilter: Joi.string(), + }), + }, + tags: ['access:uptime'], + }, + handler: async (request: any): Promise => { + const { dateRangeStart, dateRangeEnd, filters, statusFilter } = request.query; + return await libs.monitorStates.getSnapshotCount( + request, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter + ); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts new file mode 100644 index 0000000000000..934b34ef1b397 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createGetSnapshotCount } from './get_snapshot_count'; diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json index 0ac6c67e23d2b..93d63bad66e30 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 7, - "mixed": 0, - "up": 93, - "total": 100 - } - } + "up": 93, + "down": 7, + "mixed": 0, + "total": 100 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json index 8639f0ec0feea..94c1ffbc74290 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 13, - "mixed": 0, - "up": 0, - "total": 13 - } - } + "up": 0, + "down": 7, + "mixed": 0, + "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json index 8639f0ec0feea..94c1ffbc74290 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 13, - "mixed": 0, - "up": 0, - "total": 13 - } - } + "up": 0, + "down": 7, + "mixed": 0, + "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json index 065c3f90e932e..2d79880e7c0ee 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 0, - "mixed": 0, - "up": 94, - "total": 94 - } - } + "up": 93, + "down": 0, + "mixed": 0, + "total": 93 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/index.js b/x-pack/test/api_integration/apis/uptime/graphql/index.js index f7fafa9419657..346032f87dc4d 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/index.js +++ b/x-pack/test/api_integration/apis/uptime/graphql/index.js @@ -17,7 +17,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./monitor_states')); loadTestFile(require.resolve('./monitor_status_bar')); loadTestFile(require.resolve('./ping_list')); - loadTestFile(require.resolve('./snapshot')); loadTestFile(require.resolve('./snapshot_histogram')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js b/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js deleted file mode 100644 index 004b87571eab4..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js +++ /dev/null @@ -1,101 +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 { snapshotQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; - -export default function ({ getService }) { - describe('snapshot query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('will fetch a monitor snapshot summary', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - expectFixtureEql(data, 'snapshot'); - }); - - it('will fetch a monitor snapshot filtered by down status', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`, - statusFilter: 'down', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - expectFixtureEql(data, 'snapshot_filtered_by_down'); - }); - - it('will fetch a monitor snapshot filtered by up status', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`, - statusFilter: 'up', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - - expectFixtureEql(data, 'snapshot_filtered_by_up'); - }); - - it('returns null histogram data when no data present', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-25T04:30:54.740Z', - dateRangeEnd: '2025-01-28T04:50:54.740Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`, - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - - expectFixtureEql(data, 'snapshot_empty'); - }); - // TODO: test for host, port, etc. - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/index.js b/x-pack/test/api_integration/apis/uptime/index.js index 6eb77fb584133..9175658783fb5 100644 --- a/x-pack/test/api_integration/apis/uptime/index.js +++ b/x-pack/test/api_integration/apis/uptime/index.js @@ -17,5 +17,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./get_all_pings')); loadTestFile(require.resolve('./graphql')); + loadTestFile(require.resolve('./rest')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts new file mode 100644 index 0000000000000..b76d3f7c2e44a --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + describe('uptime REST endpoints', () => { + before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat')); + after('unload', () => esArchiver.unload('uptime/full_heartbeat')); + loadTestFile(require.resolve('./snapshot')); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts new file mode 100644 index 0000000000000..0175dc649b495 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('snapshot count', () => { + let dateRangeStart = '2019-01-28T17:40:08.078Z'; + let dateRangeEnd = '2025-01-28T19:00:16.078Z'; + + it('will fetch the full set of snapshot counts', async () => { + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + ); + expectFixtureEql(apiResponse.body, 'snapshot'); + }); + + it('will fetch a monitor snapshot filtered by down status', async () => { + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; + const statusFilter = 'down'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); + }); + + it('will fetch a monitor snapshot filtered by up status', async () => { + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`; + const statusFilter = 'up'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + }); + + it('returns a null snapshot when no data is present', async () => { + dateRangeStart = '2019-01-25T04:30:54.740Z'; + dateRangeEnd = '2025-01-28T04:50:54.740Z'; + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_empty'); + }); + }); +} From 2d696eb7ba854f5513773a5ef282e01270254a25 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 27 Nov 2019 08:58:49 +0100 Subject: [PATCH 61/63] [APM] Handle APM UI config keys (#51668) `xpack.apm.ui.*` keys were not properly handled due to object path parsing. --- x-pack/plugins/apm/server/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b66850ff569cb..082216a78ce5e 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -13,8 +13,11 @@ export const config = { schema: schema.object({ serviceMapEnabled: schema.boolean({ defaultValue: false }), autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), - 'ui.transactionGroupBucketSize': schema.number({ defaultValue: 100 }), - 'ui.maxTraceItems': schema.number({ defaultValue: 1000 }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + transactionGroupBucketSize: schema.number({ defaultValue: 100 }), + maxTraceItems: schema.number({ defaultValue: 1000 }), + }), }), }; @@ -30,8 +33,9 @@ export function mergeConfigs(apmOssConfig: APMOSSConfig, apmConfig: APMXPackConf 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, 'apm_oss.indexPattern': apmOssConfig.indexPattern, 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, - 'xpack.apm.ui.maxTraceItems': apmConfig['ui.maxTraceItems'], - 'xpack.apm.ui.transactionGroupBucketSize': apmConfig['ui.transactionGroupBucketSize'], + 'xpack.apm.ui.enabled': apmConfig.ui.enabled, + 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems, + 'xpack.apm.ui.transactionGroupBucketSize': apmConfig.ui.transactionGroupBucketSize, 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, }; } From 517fcb98b85c09c824c8c8e7cdad79a85d4f1cc3 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Wed, 27 Nov 2019 09:35:08 +0100 Subject: [PATCH 62/63] [SIEM] Fix Timeline drag and drop behavior (#51558) --- package.json | 9 +- packages/kbn-i18n/package.json | 2 +- packages/kbn-ui-framework/package.json | 4 +- .../plugins/kbn_tp_run_pipeline/package.json | 4 +- .../kbn_tp_custom_visualizations/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- x-pack/legacy/plugins/siem/package.json | 4 +- .../drag_and_drop/drag_drop_context.tsx | 14 + .../drag_drop_context_wrapper.tsx | 19 +- .../drag_and_drop/draggable_wrapper.tsx | 4 + .../components/drag_and_drop/helpers.test.ts | 20 +- .../components/drag_and_drop/helpers.ts | 3 + .../error_toast_dispatcher/index.test.tsx | 2 +- .../authentications_table/index.test.tsx | 2 +- .../siem/public/components/page/index.tsx | 1 - .../page/network/kpi_network/index.test.tsx | 4 +- .../network/network_dns_table/index.test.tsx | 2 +- .../network/network_http_table/index.test.tsx | 2 +- .../index.test.tsx | 4 +- .../network_top_n_flow_table/index.test.tsx | 4 +- .../page/network/tls_table/index.test.tsx | 2 +- .../page/network/users_table/index.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 4 + .../public/components/timeline/body/index.tsx | 91 +++---- .../public/components/timeline/styles.tsx | 11 +- x-pack/package.json | 8 +- yarn.lock | 241 +++++++----------- 28 files changed, 238 insertions(+), 231 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx diff --git a/package.json b/package.json index 2c8d4ad4307b1..45a376a291359 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,10 @@ "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9", - "**/deepmerge": "^4.2.2" + "**/deepmerge": "^4.2.2", + "**/react": "16.8.6", + "**/react-dom": "16.8.6", + "**/react-test-renderer": "16.8.6" }, "workspaces": { "packages": [ @@ -213,10 +216,10 @@ "pug": "^2.0.3", "querystring-browser": "1.0.4", "raw-loader": "3.1.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-addons-shallow-compare": "15.6.2", "react-color": "^2.13.8", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-grid-layout": "^0.16.2", "react-hooks-testing-library": "^0.5.0", "react-input-range": "^1.3.0", diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 591faff64711d..0146111941044 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -28,7 +28,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^2.1.0", "prop-types": "^15.6.2", - "react": "^16.8.0", + "react": "^16.8.6", "react-intl": "^2.8.0" } } diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 472c801721ecf..ee5424370fb06 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -19,7 +19,7 @@ "focus-trap-react": "^3.1.1", "lodash": "npm:@elastic/lodash@3.10.1-kibana3", "prop-types": "15.6.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-ace": "^5.9.0", "react-color": "^2.13.8", "tabbable": "1.1.3", @@ -57,7 +57,7 @@ "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-redux": "^5.0.6", "react-router": "^3.2.0", "react-router-redux": "^4.0.8", diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 769acc52e207b..97ad71eaddd7c 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -8,7 +8,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0", - "react-dom": "^16.8.0" + "react": "^16.8.6", + "react-dom": "^16.8.6" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 41e1e6baca0ec..ca584b4b4e771 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -8,6 +8,6 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0" + "react": "^16.8.6" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index a0b03e52640fc..71545fa582c66 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -9,7 +9,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0" + "react": "^16.8.6" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 952d06c4873d4..d5c97bb212ea0 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -9,7 +9,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0" + "react": "^16.8.6" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index 29c26c5f674e3..d239961ee75d7 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -12,11 +12,11 @@ "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^10.0.1" + "@types/react-beautiful-dnd": "^11.0.3" }, "dependencies": { "lodash": "^4.17.15", - "react-beautiful-dnd": "^10.0.1", + "react-beautiful-dnd": "^12.1.1", "react-markdown": "^4.0.6" } } diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx new file mode 100644 index 0000000000000..ee9533341a4f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx @@ -0,0 +1,14 @@ +/* + * 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. + */ + +// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40309 + +import { MovementMode, DraggableId } from 'react-beautiful-dnd'; + +export interface BeforeCapture { + draggableId: DraggableId; + mode: MovementMode; +} diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index c513f7a451240..a3528158a0317 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -6,10 +6,11 @@ import { defaultTo, noop } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { DragDropContext, DropResult, DragStart } from 'react-beautiful-dnd'; +import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; +import { BeforeCapture } from './drag_drop_context'; import { BrowserFields } from '../../containers/source'; import { dragAndDropModel, dragAndDropSelectors } from '../../store'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; @@ -20,6 +21,7 @@ import { addProviderToTimeline, fieldWasDroppedOnTimelineColumns, IS_DRAGGING_CLASS_NAME, + IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, providerWasDroppedOnTimeline, providerWasDroppedOnTimelineButton, draggableIsField, @@ -75,11 +77,16 @@ export const DragDropContextWrapperComponent = React.memo( if (!draggableIsField(result)) { document.body.classList.remove(IS_DRAGGING_CLASS_NAME); } + + if (draggableIsField(result)) { + document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } }, [browserFields, dataProviders] ); return ( - + // @ts-ignore + {children} ); @@ -107,7 +114,7 @@ const mapStateToProps = (state: State) => { export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent); -const onDragStart = (initial: DragStart) => { +const onBeforeCapture = (before: BeforeCapture) => { const x = window.pageXOffset !== undefined ? window.pageXOffset @@ -120,9 +127,13 @@ const onDragStart = (initial: DragStart) => { window.onscroll = () => window.scrollTo(x, y); - if (!draggableIsField(initial)) { + if (!draggableIsField(before)) { document.body.classList.add(IS_DRAGGING_CLASS_NAME); } + + if (draggableIsField(before)) { + document.body.classList.add(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } }; const enableScrolling = () => (window.onscroll = () => noop); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 0f0e61e0206ec..c314785511201 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -34,6 +34,10 @@ export const useDraggablePortalContext = () => useContext(DraggablePortalContext const Wrapper = styled.div` display: inline-block; max-width: 100%; + + [data-rbd-placeholder-context-id] { + display: none !important; + } `; Wrapper.displayName = 'Wrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts index 8d3334b05bfaf..af4b9b280f3cd 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts @@ -116,7 +116,7 @@ describe('helpers', () => { test('it returns false when the draggable is NOT content', () => { expect( draggableIsContent({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { @@ -230,10 +230,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns false when the destination is null', () => { + test('it returns false when the destination is undefined', () => { expect( destinationIsTimelineProviders({ - destination: null, + destination: undefined, draggableId: getDraggableId('685260508808089'), reason: 'DROP', source: { @@ -286,10 +286,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns returns false when the destination is null', () => { + test('it returns returns false when the destination is undefined', () => { expect( destinationIsTimelineColumns({ - destination: null, + destination: undefined, draggableId: getDraggableFieldId({ contextId: 'test', fieldId: 'event.action' }), reason: 'DROP', source: { @@ -342,10 +342,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns false when the destination is null', () => { + test('it returns false when the destination is undefined', () => { expect( destinationIsTimelineButton({ - destination: null, + destination: undefined, draggableId: getDraggableId('685260508808089'), reason: 'DROP', source: { @@ -436,10 +436,10 @@ describe('helpers', () => { ).toEqual('timeline'); }); - test('it returns returns an empty string when the destination is null', () => { + test('it returns returns an empty string when the destination is undefined', () => { expect( getTimelineIdFromDestination({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { @@ -558,7 +558,7 @@ describe('helpers', () => { test('it returns false when the draggable is NOT content', () => { expect( providerWasDroppedOnTimeline({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts index 415970474db4c..ae3a8828491e3 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts @@ -224,3 +224,6 @@ export const DRAG_TYPE_FIELD = 'drag-type-field'; /** This class is added to the document body while dragging */ export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; + +/** This class is added to the document body while timeline field dragging */ +export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx index 6c4bc797d39f8..6233fcfe7c823 100644 --- a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx @@ -30,7 +30,7 @@ describe('Error Toast Dispatcher', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(ErrorToastDispatcherComponent)'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx index 7dd5eccb4a6c6..71e61e2425373 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx @@ -49,7 +49,7 @@ describe('Authentication Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(AuthenticationTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index d56012de88929..582ef2d01fb7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -19,7 +19,6 @@ import styled, { createGlobalStyle } from 'styled-components'; SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ - export const AppGlobalStyle = createGlobalStyle` div.app-wrapper { background-color: rgba(0,0,0,0); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx index eb6204044bdb7..964617c4c85b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx @@ -42,7 +42,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); }); test('it renders the default widget', () => { @@ -59,7 +59,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx index 98f55b29c8fc4..8bf338d17c47b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx @@ -51,7 +51,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkDnsTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx index 277e136d776fa..c92661a909a6e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx @@ -51,7 +51,7 @@ describe('NetworkHttp Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkHttpTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx index d8a5da6036f95..ca7a3c0bb4387 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -57,7 +57,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); }); test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( @@ -82,7 +82,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx index df9e0f9f89645..884825422beb0 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx @@ -57,7 +57,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); }); test('it renders the default NetworkTopNFlow table on the IP Details page', () => { @@ -83,7 +83,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx index 612896c878ef9..8c397053380c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx @@ -47,7 +47,7 @@ describe('Tls Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(TlsTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx index 00a0a34a2b30b..d178164fd3fd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx @@ -55,7 +55,7 @@ describe('Users Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(UsersTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 8bf7b1543b923..65818b697e0b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -483,8 +483,12 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 47fbcec4aab23..07e37346ac968 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -19,7 +19,7 @@ import { OnUnPinEvent, OnUpdateColumns, } from '../events'; -import { EventsTable, TimelineBody } from '../styles'; +import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { ColumnHeader } from './column_headers/column_header'; import { Events } from './events'; @@ -86,50 +86,53 @@ export const Body = React.memo( ); return ( - - - + <> + + + - - - + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index 856259f11ced9..1c1c8fac75cdc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -6,7 +6,9 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; -import styled from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; + +import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; /** * OFFSET PIXEL VALUES @@ -18,6 +20,13 @@ export const OFFSET_SCROLLBAR = 17; * TIMELINE BODY */ +// SIDE EFFECT: the following creates a global class selector +export const TimelineBodyGlobalStyle = createGlobalStyle` + body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .siemTimeline__body { + overflow: hidden; + } +`; + export const TimelineBody = styled.div.attrs(({ className }) => ({ className: `siemTimeline__body ${className}`, }))<{ bodyHeight: number }>` diff --git a/x-pack/package.json b/x-pack/package.json index d97fd38676bde..c5114500c6f61 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -92,7 +92,7 @@ "@types/react-resize-detector": "^4.0.1", "@types/react-router-dom": "^4.3.1", "@types/react-sticky": "^6.0.3", - "@types/react-test-renderer": "^16.8.0", + "@types/react-test-renderer": "^16.8.3", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", @@ -153,7 +153,7 @@ "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-hooks-testing-library": "^0.3.8", - "react-test-renderer": "^16.8.0", + "react-test-renderer": "^16.8.6", "react-testing-library": "^6.0.0", "sass-loader": "^7.3.1", "sass-resources-loader": "^2.0.1", @@ -293,11 +293,11 @@ "puid": "1.0.7", "puppeteer-core": "^1.19.0", "raw-loader": "3.1.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", "react-datetime": "^2.14.0", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", "react-markdown": "^3.4.1", diff --git a/yarn.lock b/yarn.lock index 1cf41a3ecd57c..e12a0eb46c6cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,6 +982,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.2" +"@babel/runtime-corejs2@^7.6.3": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.7.4.tgz#b9c2b1b5882762005785bc47740195a0ac780888" + integrity sha512-hKNcmHQbBSJFnZ82ewYtWDZ3fXkP/l1XcfRtm7c8gHPM/DMecJtFFBEp7KMLZTuHwwb7RfemHdsEnd7L916Z6A== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" + "@babel/runtime@7.0.0-beta.54": version "7.0.0-beta.54" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf" @@ -3735,13 +3743,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^10.0.1": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.2.tgz#74069f7b1d0cb67b7af99a2584b30e496e545d8b" - integrity sha512-76M5VRbhduUarM9wyMWQm3tLKCVMKTlhG0+W67dteg/HBE+kueIwuyLWzE0m5fmuilvrDXoM5NL890KLnHETZw== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^10.1.0": version "10.1.1" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#7afae39a4247f30c13b8bbb726ccd1b8cda9d4a5" @@ -3749,6 +3750,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@^11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#51d9f37942dd18cc4aa10da98a5c883664e7ee46" + integrity sha512-7ZbT/7mNJu+uRrUGdTQ1hAINtqg909L4NHrXyspV42fvVgBgda6ysiBzoDUMENmQ/RlRJdpyrcp8Dtd/77bp9Q== + dependencies: + "@types/react" "*" + "@types/react-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" @@ -3829,10 +3837,10 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@^16.8.0": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.1.tgz#96f3ce45a3a41c94eca532a99103dd3042c9d055" - integrity sha512-8gU69ELfJGxzVWVYj4MTtuHxz9nO+d175XeQ1XrXXxesUBsB4KK6OCfzVhEX6leZWWBDVtMJXp/rUjhClzL7gw== +"@types/react-test-renderer@^16.8.3": + version "16.9.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" + integrity sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ== dependencies: "@types/react" "*" @@ -4533,7 +4541,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.0.0.tgz#c8ba6f0f1aac4b0a9e32d1f0af12be769528f36b" integrity sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg== -acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.2.1, acorn@^5.5.0: +acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.5.0: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== @@ -6366,11 +6374,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base62@^1.1.0: - version "1.2.8" - resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" - integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== - base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -8224,11 +8227,6 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, comm resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.5.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -8258,21 +8256,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -commoner@^0.10.1: - version "0.10.8" - resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" - integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU= - dependencies: - commander "^2.5.0" - detective "^4.3.1" - glob "^5.0.15" - graceful-fs "^4.1.2" - iconv-lite "^0.4.5" - mkdirp "^0.5.0" - private "^0.1.6" - q "^1.1.2" - recast "^0.11.17" - compare-versions@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" @@ -8976,6 +8959,13 @@ css-box-model@^1.1.1: dependencies: tiny-invariant "^1.0.3" +css-box-model@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0" + integrity sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -9805,7 +9795,7 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@^1.0.0, defined@~1.0.0: +defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= @@ -10054,14 +10044,6 @@ detective-typescript@^5.1.1: node-source-walk "^4.2.0" typescript "^3.4.5" -detective@^4.3.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" - integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== - dependencies: - acorn "^5.2.1" - defined "^1.0.0" - dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -10740,14 +10722,6 @@ env-variable@0.0.x: resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA== -envify@^3.0.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8" - integrity sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg= - dependencies: - jstransform "^11.0.3" - through "~2.3.4" - enzyme-adapter-react-16@^1.15.1: version "1.15.1" resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" @@ -11439,11 +11413,6 @@ espree@^6.1.1: acorn-jsx "^5.0.2" eslint-visitor-keys "^1.1.0" -esprima-fb@^15001.1.0-dev-harmony-fb: - version "15001.1.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" - integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE= - esprima@2.7.x, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -12118,17 +12087,6 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7" - integrity sha1-lja3cF9bqWhNRLcveDISVK/IYPc= - dependencies: - core-js "^1.0.0" - loose-envify "^1.0.0" - promise "^7.0.3" - ua-parser-js "^0.7.9" - whatwg-fetch "^0.9.0" - fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" @@ -14999,7 +14957,7 @@ icalendar@0.7.1: resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" integrity sha1-0NNIZ5X48cXPT4yvrAgbS056Mq4= -iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -17246,17 +17204,6 @@ jssha@^2.1.0: resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.3.1.tgz#147b2125369035ca4b2f7d210dc539f009b3de9a" integrity sha1-FHshJTaQNcpLL30hDcU58Amz3po= -jstransform@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" - integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM= - dependencies: - base62 "^1.1.0" - commoner "^0.10.1" - esprima-fb "^15001.1.0-dev-harmony-fb" - object-assign "^2.0.0" - source-map "^0.4.2" - jstransformer-ejs@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/jstransformer-ejs/-/jstransformer-ejs-0.0.3.tgz#04d9201469274fcf260f1e7efd732d487fa234b6" @@ -18844,6 +18791,11 @@ memoize-one@^5.0.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.2.tgz#6aba5276856d72fb44ead3efab86432f94ba203d" integrity sha512-o7lldN4fs/axqctc03NF+PMhd2veRrWeJ2n2GjEzUPBD4F9rmNg4A+bQCACIzwjHJEXuYv4aFFMaH35KZfHUrw== +memoize-one@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + memoizee@0.4.X: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" @@ -21954,7 +21906,7 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.9.0" function-bind "^1.1.1" -promise@^7.0.1, promise@^7.0.3, promise@^7.1.1: +promise@^7.0.1, promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== @@ -22405,6 +22357,11 @@ raf-schd@^4.0.0: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.0.tgz#9855756c5045ff4ed4516e14a47719387c3c907b" integrity sha512-m7zq0JkIrECzw9mO5Zcq6jN4KayE34yoIS9hJoiZNXyOAT06PPA8PrR+WtJIeFW09YjUfNkMMN9lrmAt6BURCA== +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + raf@^3.1.0, raf@^3.3.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" @@ -22600,7 +22557,7 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^10.0.1, react-beautiful-dnd@^10.1.0: +react-beautiful-dnd@^10.1.0: version "10.1.1" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#d753088d77d7632e77cf8a8935fafcffa38f574b" integrity sha512-TdE06Shfp56wm28EzjgC56EEMgGI5PDHejJ2bxuAZvZr8CVsbksklsJC06Hxf0MSL7FHbflL/RpkJck9isuxHg== @@ -22614,6 +22571,19 @@ react-beautiful-dnd@^10.0.1, react-beautiful-dnd@^10.1.0: redux "^4.0.1" tiny-invariant "^1.0.4" +react-beautiful-dnd@^12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#810f9b9d94f667b15b253793e853d016a0f3f07c" + integrity sha512-w/mpIXMEXowc53PCEnMoFyAEYFgxMfygMK5msLo5ifJ2/CiSACLov9A79EomnPF7zno3N207QGXsraBxAJnyrw== + dependencies: + "@babel/runtime-corejs2" "^7.6.3" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-beautiful-dnd@^8.0.7: version "8.0.7" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" @@ -22738,17 +22708,7 @@ react-docgen@^4.1.0: node-dir "^0.1.10" recast "^0.17.3" -react-dom@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.2.tgz#7c8a69545dd554d45d66442230ba04a6a0a3c3d3" - integrity sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.2" - -react-dom@^16.8.3, react-dom@^16.8.5: +react-dom@16.8.6, react-dom@^16.8.3, react-dom@^16.8.5, react-dom@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== @@ -22911,7 +22871,7 @@ react-is@^16.10.2, react-is@^16.9.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.6: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== @@ -23088,6 +23048,18 @@ react-redux@^5.1.1: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-resizable@1.x: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -23236,7 +23208,7 @@ react-syntax-highlighter@^8.0.1: prismjs "^1.8.4" refractor "^2.4.1" -react-test-renderer@^16.0.0-0: +react-test-renderer@16.8.6, react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1" integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw== @@ -23246,16 +23218,6 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.6" scheduler "^0.13.6" -react-test-renderer@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.2.tgz#3ce0bf12aa211116612fda01a886d6163c9c459b" - integrity sha512-gsd4NoOaYrZD2R8zi+CBV9wTGMsGhE2bRe4wvenGy0WcLJgdPscRZDDz+kmLjY+/5XpYC8yRR/v4CScgYfGyoQ== - dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.2" - scheduler "^0.13.2" - react-testing-library@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d" @@ -23343,25 +23305,7 @@ react-visibility-sensor@^5.1.1: dependencies: prop-types "^15.7.2" -react@^0.14.0: - version "0.14.9" - resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1" - integrity sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE= - dependencies: - envify "^3.0.0" - fbjs "^0.6.1" - -react@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c" - integrity sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.2" - -react@^16.8.3, react@^16.8.5: +react@16.8.6, react@^0.14.0, react@^16.8.3, react@^16.8.5, react@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== @@ -23594,16 +23538,6 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" -recast@^0.11.17, recast@~0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -23624,6 +23558,16 @@ recast@^0.17.3: private "^0.1.8" source-map "~0.6.1" +recast@~0.11.12: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -23749,6 +23693,14 @@ redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -24809,7 +24761,7 @@ saxes@^3.1.3: dependencies: xmlchars "^2.1.1" -scheduler@^0.13.2, scheduler@^0.13.3, scheduler@^0.13.6: +scheduler@^0.13.3, scheduler@^0.13.6: version "0.13.6" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== @@ -26987,6 +26939,11 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== +tiny-invariant@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" + integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + tiny-lr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" @@ -28407,6 +28364,11 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" @@ -29570,11 +29532,6 @@ whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== -whatwg-fetch@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" - integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA= - whatwg-mimetype@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" From 93654d5ff977a2f0f036240d97dc32887231174e Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 27 Nov 2019 09:49:54 +0100 Subject: [PATCH 63/63] [ML] Fix anomaly detection test suite (#51712) This PR re-enables the anomaly detection test suite and disables Firefox test execution for now. It also increases stability for `clickEditDetector` and removes unneeded retries. --- .../anomaly_detection/index.ts | 5 +++-- .../machine_learning/job_management.ts | 6 +----- .../machine_learning/job_wizard_advanced.ts | 20 ++++++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index 2b76bce544f6d..d5d617587fc3b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -6,8 +6,9 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - // FLAKY: https://github.com/elastic/kibana/issues/51669 - describe.skip('anomaly detection', function() { + describe('anomaly detection', function() { + this.tags(['skipFirefox']); + loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index ddab5fd68f13c..5ffb235a828d6 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -15,7 +15,6 @@ export function MachineLearningJobManagementProvider( mlApi: ProvidedType ) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); return { async navigateToNewJobSourceSelection() { @@ -36,10 +35,7 @@ export function MachineLearningJobManagementProvider( }, async assertStartDatafeedModalExists() { - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlStartDatafeedModal'); - }); + await testSubjects.existOrFail('mlStartDatafeedModal', { timeout: 5000 }); }, async confirmStartDatafeedModal() { diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts index 71b76a6885592..3e7dacb23d61b 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts @@ -146,10 +146,7 @@ export function MachineLearningJobWizardAdvancedProvider({ }, async assertCreateDetectorModalExists() { - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlCreateDetectorModal'); - }); + await testSubjects.existOrFail('mlCreateDetectorModal', { timeout: 5000 }); }, async assertDetectorFunctionInputExists() { @@ -298,18 +295,17 @@ export function MachineLearningJobWizardAdvancedProvider({ }, async clickEditDetector(detectorIndex: number) { - await testSubjects.click( - `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` - ); - await this.assertCreateDetectorModalExists(); + await retry.tryForTime(20 * 1000, async () => { + await testSubjects.click( + `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` + ); + await this.assertCreateDetectorModalExists(); + }); }, async createJob() { await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlStartDatafeedModal'); - }); + await testSubjects.existOrFail('mlStartDatafeedModal', { timeout: 10 * 1000 }); }, }; }