From a8380cf52b77ffd000ce0633475e6e3220a4c1c7 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 27 Feb 2020 16:08:37 +0100 Subject: [PATCH 01/34] [Discover] Fix incorrect filter generated by "Filter for field present" (#58586) * Fix _exists_ filter handling * Add functional test --- .../filter_manager/lib/generate_filters.ts | 5 +++-- test/functional/apps/context/_filters.js | 16 +++++++++++++--- test/functional/services/doc_table.ts | 19 +++++++++++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index 105e932f696f07..4220df7b1a49b7 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -103,11 +103,12 @@ export function generateFilters( filter = existing; } else { const tmpIndexPattern = { id: index } as IIndexPattern; - + // exists filter special case: fieldname = '_exists' and value = fieldname const filterType = fieldName === '_exists_' ? FILTERS.EXISTS : FILTERS.PHRASE; + const actualFieldObj = fieldName === '_exists_' ? ({ name: value } as IFieldType) : fieldObj; filter = buildFilter( tmpIndexPattern, - fieldObj, + actualFieldObj, filterType, negate, false, diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index 4cd3f1a54b7717..c9499f5a805ab3 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -39,14 +39,13 @@ export default function({ getService, getPageObjects }) { }); }); - it('should be addable via expanded doc table rows', async function() { + it('inclusive filter should be addable via expanded doc table rows', async function() { await docTable.toggleRowExpanded({ isAnchorRow: true }); await retry.try(async () => { const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - // await docTable.toggleRowExpanded({ isAnchorRow: true }); expect( await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true) ).to.be(true); @@ -58,7 +57,7 @@ export default function({ getService, getPageObjects }) { }); }); - it('should be toggleable via the filter bar', async function() { + it('inclusive filter should be toggleable via the filter bar', async function() { await filterBar.addFilter(TEST_ANCHOR_FILTER_FIELD, 'IS', TEST_ANCHOR_FILTER_VALUE); await PageObjects.context.waitUntilContextLoadingHasFinished(); // disable filter @@ -76,5 +75,16 @@ export default function({ getService, getPageObjects }) { expect(hasOnlyFilteredRows).to.be(false); }); }); + + it('filter for presence should be addable via expanded doc table rows', async function() { + await docTable.toggleRowExpanded({ isAnchorRow: true }); + + await retry.try(async () => { + const anchorDetailsRow = await docTable.getAnchorDetailsRow(); + await docTable.addExistsFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true)).to.be(true); + }); + }); }); } diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts index 6957b0fa999294..2530831e0f6f9b 100644 --- a/test/functional/services/doc_table.ts +++ b/test/functional/services/doc_table.ts @@ -98,13 +98,12 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont const $ = await table.parseDomContent(); const rowLocator = options.isAnchorRow ? '~docTableAnchorRow' : '~docTableRow'; const rows = $.findTestSubjects(rowLocator).toArray(); - const fields = rows.map((row: any) => + return rows.map((row: any) => $(row) .find('[data-test-subj~="docTableField"]') .toArray() .map((field: any) => $(field).text()) ); - return fields; } public async getHeaderFields(): Promise { @@ -144,6 +143,22 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + public async getAddExistsFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByCssSelector(`[data-test-subj~="addExistsFilterButton"]`); + } + + public async addExistsFilter( + detailsRow: WebElementWrapper, + fieldName: WebElementWrapper + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddExistsFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + public async toggleRowExpanded( options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } ): Promise { From a06cc315832b178ab7b3f46610d92e4b5a491837 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Thu, 27 Feb 2020 11:09:43 -0500 Subject: [PATCH 02/34] [SIEM] [Detection Engine] Remove unnecessary ts-ignores (#58689) * fixes from comments on previous PR * replaces any with unknown, adds test to show how optional chaining of array items can result in undefined even though typescript says its not --- .../routes/rules/find_rules_status_route.ts | 4 ++++ .../lib/detection_engine/routes/utils.test.ts | 16 ++++++++++++++++ .../server/lib/detection_engine/routes/utils.ts | 5 +++-- .../detection_engine/rules/read_rules.test.ts | 9 ++++----- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index c496c7b7ce59cd..5687c5d4095db1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -59,6 +59,10 @@ export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.S searchFields: ['alertId'], }); const accumulated = await acc; + + // Array accessors can result in undefined but + // this is not represented in typescript for some reason, + // https://github.com/Microsoft/TypeScript/issues/11122 const currentStatus = convertToSnakeCase( lastFiveErrorsForId.saved_objects[0]?.attributes ); 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 3148083b4db267..a382c4a3236719 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,6 +6,8 @@ import Boom from 'boom'; +import { SavedObjectsFindResponse } from 'kibana/server'; +import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; import { transformError, transformBulkError, @@ -323,5 +325,19 @@ describe('utils', () => { const values = {}; expect(convertToSnakeCase(values)).toEqual({}); }); + it('returns null when passed in undefined', () => { + // Array accessors can result in undefined but + // this is not represented in typescript for some reason, + // https://github.com/Microsoft/TypeScript/issues/11122 + const values: SavedObjectsFindResponse = { + page: 0, + per_page: 5, + total: 0, + saved_objects: [], + }; + expect( + convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not + ).toEqual(null); + }); }); }); 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 aaa5db7966b2b3..65c9141619cb9e 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 @@ -222,8 +222,9 @@ export const getIndex = (getSpaceId: () => string, config: LegacyServices['confi return `${signalsIndex}-${spaceId}`; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const convertToSnakeCase = >(obj: T): Partial | null => { +export const convertToSnakeCase = >( + obj: T +): Partial | null => { if (!obj) { return null; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index aa1cce6f152382..862ea9d2dcbe5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -39,9 +39,9 @@ describe('read_rules', () => { }); test('should return null if saved object found by alerts client given id is not alert type', async () => { const alertsClient = alertsClientMock.create(); - const { alertTypeId, ...rest } = getResult(); - // @ts-ignore - alertsClient.get.mockImplementation(() => rest); + const result = getResult(); + delete result.alertTypeId; + alertsClient.get.mockResolvedValue(result); const rule = await readRules({ alertsClient, @@ -109,8 +109,7 @@ describe('read_rules', () => { test('should return null if the output from alertsClient with ruleId set is empty', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - // @ts-ignore - alertsClient.find.mockResolvedValue({ data: [] }); + alertsClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); const rule = await readRules({ alertsClient, From 515348438bce26b3c09989ba85b58dd79f6de0da Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 27 Feb 2020 08:14:45 -0800 Subject: [PATCH 03/34] Fixed connector and alerts view flashing empty state before loading list (#58693) --- .../components/actions_connectors_list.tsx | 4 +++- .../sections/alerts_list/components/alerts_list.tsx | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index bed285f668e01f..f48e27791419dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -14,6 +14,7 @@ import { EuiEmptyPrompt, EuiTitle, EuiLink, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -370,8 +371,9 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { /> {/* Render the view based on if there's data or if they can save */} + {(isLoadingActions || isLoadingActionTypes) && } {data.length !== 0 && table} - {data.length === 0 && canSave && emptyPrompt} + {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && emptyPrompt} {data.length === 0 && !canSave && noPermissionPrompt} { {convertAlertsToTableItems(alertsState.data, alertTypesState.data).length !== 0 && table} {convertAlertsToTableItems(alertsState.data, alertTypesState.data).length === 0 && + !alertTypesState.isLoading && + !alertsState.isLoading && emptyPrompt} + {(alertTypesState.isLoading || alertsState.isLoading) && } Date: Thu, 27 Feb 2020 08:28:08 -0800 Subject: [PATCH 04/34] [DOCS] Fixes outdated monitoring links (#58556) --- docs/user/monitoring/elasticsearch-details.asciidoc | 2 +- .../server/spec/generated/monitoring.bulk.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index 2990e965be03c4..c0e804672d2986 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -14,7 +14,7 @@ the <>, <>, [role="screenshot"] image::user/monitoring/images/monitoring-elasticsearch.jpg["Monitoring clusters"] -See also {ref}/es-monitoring.html[Monitoring {es}]. +See also {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. [float] [[cluster-overview-page]] diff --git a/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json b/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json index 2b27950e7b0974..26a9078f73ce82 100644 --- a/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json +++ b/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json @@ -12,6 +12,6 @@ "patterns": [ "_monitoring/bulk" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/es-monitoring.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/monitor-elasticsearch-cluster.html" } } From 43be649f5006219421edaf151115d8240b0d7d57 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 27 Feb 2020 09:32:44 -0700 Subject: [PATCH 05/34] [Metrics UI] Ensure inventory view buckets never drop below 60 seconds (#58503) * [Metrics UI] Ensure inventory view buckets never drop below 60 seconds * Fixing tests * Fixing tests... again * Fixing tests... rounding issue? * Trying to fix the tests... again * updating test for custom metric Co-authored-by: Elastic Machine --- .../create_timerange_with_interval.ts | 6 +++-- .../test/api_integration/apis/infra/waffle.ts | 24 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index 6f036475a1e1cf..cf2b1e59b2a225 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -21,7 +21,7 @@ export const createTimeRangeWithInterval = async ( ): Promise => { const aggregations = getMetricsAggregations(options); const modules = await aggregationsToModules(framework, requestContext, aggregations, options); - const interval = + const interval = Math.max( (await calculateMetricInterval( framework, requestContext, @@ -32,7 +32,9 @@ export const createTimeRangeWithInterval = async ( }, modules, options.nodeType - )) || 60000; + )) || 60, + 60 + ); return { interval: `${interval}s`, from: options.timerange.to - interval * 5000, // We need at least 5 buckets worth of data diff --git a/x-pack/test/api_integration/apis/infra/waffle.ts b/x-pack/test/api_integration/apis/infra/waffle.ts index 3413fc283556c7..26d8c9d265a6a6 100644 --- a/x-pack/test/api_integration/apis/infra/waffle.ts +++ b/x-pack/test/api_integration/apis/infra/waffle.ts @@ -192,9 +192,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'cpu', - value: 0.009285714285714286, - max: 0.009285714285714286, - avg: 0.0015476190476190477, + value: 0.0032, + max: 0.0038333333333333336, + avg: 0.0027944444444444444, }); } }); @@ -231,9 +231,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'custom', - value: 0.0041964285714285714, - max: 0.0041964285714285714, - avg: 0.0006994047619047619, + value: 0.0016, + max: 0.0018333333333333333, + avg: 0.0013666666666666669, }); } }); @@ -320,9 +320,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'cpu', - value: 0.009285714285714286, - max: 0.009285714285714286, - avg: 0.0015476190476190477, + value: 0.0032, + max: 0.0038333333333333336, + avg: 0.0027944444444444444, }); const secondNode = nodes[1]; expect(secondNode).to.have.property('path'); @@ -332,9 +332,9 @@ export default function({ getService }: FtrProviderContext) { expect(secondNode).to.have.property('metric'); expect(secondNode.metric).to.eql({ name: 'cpu', - value: 0.009285714285714286, - max: 0.009285714285714286, - avg: 0.0015476190476190477, + value: 0.0032, + max: 0.0038333333333333336, + avg: 0.0027944444444444444, }); } }); From 235b3535e2fb59894a4624f4d56183502adbee47 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 27 Feb 2020 10:40:07 -0600 Subject: [PATCH 06/34] [DOCS] Consolidates map content (#57736) * [DOCS] Consolidates map content * Section reorganization * Navigation options * Added images and cleaned up text * Added tilemap.html to redirects * Review comments --- .../visualize_coordinate_map_example.png | Bin 0 -> 397956 bytes docs/images/visualize_heat_map_example.png | Bin 0 -> 51515 bytes docs/images/visualize_region_map_example.png | Bin 0 -> 437327 bytes docs/redirects.asciidoc | 5 +- docs/setup/settings.asciidoc | 8 +- docs/user/visualize.asciidoc | 8 +- docs/visualize/heatmap.asciidoc | 40 ----- docs/visualize/metric.asciidoc | 16 -- docs/visualize/regionmap.asciidoc | 53 ------- docs/visualize/tilemap.asciidoc | 139 +++++++++++++++--- 10 files changed, 128 insertions(+), 141 deletions(-) create mode 100644 docs/images/visualize_coordinate_map_example.png create mode 100644 docs/images/visualize_heat_map_example.png create mode 100644 docs/images/visualize_region_map_example.png delete mode 100644 docs/visualize/heatmap.asciidoc delete mode 100644 docs/visualize/metric.asciidoc delete mode 100644 docs/visualize/regionmap.asciidoc diff --git a/docs/images/visualize_coordinate_map_example.png b/docs/images/visualize_coordinate_map_example.png new file mode 100644 index 0000000000000000000000000000000000000000..24f03376adadeb3bdc32ca480a8054977ac41242 GIT binary patch literal 397956 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 z z!*kC)XU?NP`?Ei@*-^aZ->zkKU;gr!tux51ADm}5CT5>=ardk|KAZ6}YmwPY%+kU+rVUag*Cq|l zlchP}gL4b4aH4r|9rNE@U6=e?_3Sg&Zh;|=)!TB0ff03kd2uG}-2YVA|M271W}LgM z8K2BR_Onw`9VRa6^P#A$JWjo5owh5?6xy-1M|z=UT3WWu(go}W`7A8m4p(l@Ygy?* z)e-F}wPVCsbPV7V*<8nP`1mt?GXw1Dmj>b%VbONKEpoVc9$d{iF5V|+mcp?1Z(<|w zfxf$8aOaT9Y(7lPENb6BEep%_!8?sB@)m{lEZcT?h@ELM|MmE;_pFO+f?^p)W%o%{ z@7YGcwT!>A`}JgM+YYFnkzYQv!vi!rg)0uNh7O>jhQS5FKB>*gyxcS)Old^0-8+Wl z*~aklT#Dj}y4NlOT?D!abP;G90qiTY6_T*Y27>+SK$@NH8_*zkIP5t%rlq;Jh{H?ZxgJoejV_3d{1IPn2WuR5>G_F|3aoFH&ffnfqMI9*akq##d&KDdy zIBwV{h_uwX4?p}+CoSpJJn1s^&(m;auO0RsVSg>0HFH}2eo3?O&dCAA9z4+c<3Ijm z`we|&B0=}+)vM-k`kTM`8$14;eOaD+{<-i|KlM}Lhd=yb_>JHA4cn B|z(z{W5>qATW`M={`G(Mud^8u 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

>m+|Fa4CI5 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

*r=!lU#7N=j{}^-m7=TjYxc3t z`77HvfPe$-M};koMegJ{)7Yj4NsX`RbdB?C7+*`;7Jh|x;1|YMXwE26Ca(k>;$u9# zLfh)Q^w<`4!mNETHoiiK_&AlSQ+dV#mCK*kNsYs)+oENwtwnxgtD^qmeb*a}`XwQu zz>kjttP|?iu4~0ri{0TVZ{yy*Av+d?v z?zG?E{C}=({H{!cuDaq934j{80Oz(l{$w56x3za=E`nwZw=xZW>1AnVv~Yn+&mPD5 zCN&&Jv577rL@Xvp7gM8JrkeS(Hzf}v(g~=1%b`zyf3k&@!bS|P{ zo$%&c!)3ZQMnVh(L$$TRU2b=1GtG@qtPe1?BCt_D^^~46p?S!r$)xG6x5wB$f4;*` zQ`}!~q!2;Mi|*-w6z4t2rK?RVZS=e|uO zXqX{CK6lw)|JKhZnGj~tXt2B`lR0pyS0_iNXmjlVwN1BUkC7?bJR3AcNbmM*ELuM4;t*6Y5DniYTMN^ zQMy&8YF)MNsHn2{leW<$3E7rgkK?+!@XHifQ`~&(?bcnq##*_l99OTDHWB`Ys_P&X z!N)7FzUCTa2r*ex<1?pgmm}rBCtsJ6)KS|lv!~_1Y4_cu_4vuXPP9Azc#nOyaG^}8 zM%m53zn-EzfHOCj?XgD{nQFDR;lp0C#~*!AS~SbNPL-wcg>Zh@n{TNi<)_y5 z=*}+u8znP5U=1xXp^@Y4r1!C zx!YD8z-?>l1g`Ns@j7h@hp^d?=b?^qc%|r&(AD_@;R)Ag2Yv|Gzz6


hGzb|Amf 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 diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 0749ae1d30e9e2..1eabf2441da4f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -17,6 +17,7 @@ import { EuiButtonEmpty, EuiButton, EuiFlyoutBody, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; @@ -146,6 +147,17 @@ export const ConnectorAddFlyout = () => { actionTypeName: actionType.name, }} /> +   + @@ -159,6 +171,17 @@ export const ConnectorAddFlyout = () => { defaultMessage="Select a connector" id="xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle" /> +   + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 55386ec6d61f9e..64862927256603 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup } from '@elastic/eui'; +import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup, EuiBetaBadge } from '@elastic/eui'; import { EuiModal, EuiButton, @@ -129,6 +129,17 @@ export const ConnectorAddModal = ({ actionTypeName: actionType.name, }} /> +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index f7ad6f95d048f8..6fe555fd74b398 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -16,6 +16,7 @@ import { EuiFlyoutFooter, EuiButtonEmpty, EuiButton, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; @@ -96,6 +97,17 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => defaultMessage="Edit connector" id="xpack.triggersActionsUI.sections.editConnectorForm.flyoutTitle" /> +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx index a88f916346985a..20ba9f5a497153 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx @@ -16,6 +16,7 @@ import { EuiButton, EuiFlyoutBody, EuiPortal, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; @@ -136,6 +137,16 @@ export const AlertAdd = ({ consumer, canChangeTrigger, alertTypeId }: AlertAddPr defaultMessage="Create Alert" id="xpack.triggersActionsUI.sections.alertAdd.flyoutTitle" /> +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index d2cf2decc4a160..2625768dc72421 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,9 +8,17 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; import { Alert, ActionType } from '../../../../types'; -import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui'; +import { + EuiTitle, + EuiBadge, + EuiFlexItem, + EuiButtonEmpty, + EuiSwitch, + EuiBetaBadge, +} from '@elastic/eui'; import { times, random } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; jest.mock('../../../app_context', () => ({ useAppDependencies: jest.fn(() => ({ @@ -54,7 +62,19 @@ describe('alert_details', () => { ).containsMatchingElement( -

{alert.name}

+

+ {alert.name} +   + +

) ).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 9c3b69962879f3..1952e35c229242 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -21,8 +21,10 @@ import { EuiSwitch, EuiCallOut, EuiSpacer, + EuiBetaBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasSaveAlertsCapability } from '../../../lib/capabilities'; import { Alert, AlertType, ActionType } from '../../../../types'; @@ -66,7 +68,20 @@ export const AlertDetails: React.FunctionComponent = ({ -

{alert.name}

+

+ {alert.name} +   + +

diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 307f39382a2363..f049406b639c79 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await log.debug('Checking for section heading to say Triggers and Actions.'); const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText(); - expect(headingText).to.be('Alerts and Actions'); + expect(headingText).to.be('Alerts and Actions BETA'); }); describe('Connectors tab', () => { From 841e64e0c1a56e00b50394ff85567f2f6a20faab Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 27 Feb 2020 14:19:24 -0700 Subject: [PATCH 13/34] =?UTF-8?q?run=20jest=20with=20`--detectOpenHandles`?= =?UTF-8?q?=20on=20CI=20to=20figure=20out=20what=20i=E2=80=A6=20(#58543)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * run jest with `--detectOpenHandles` on CI to figure out what is happening with pauses * focus tests on jest integration * force kill child processes in config reload test * skip flaky suite * increase timeout for looking for installed packages * run all tests again --- .../reload_logging_config.test.ts | 318 +++++++++--------- .../installed_packages.test.ts | 2 +- tasks/test_jest.js | 2 +- test/scripts/jenkins_xpack.sh | 2 +- 4 files changed, 159 insertions(+), 165 deletions(-) diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index 2def3569828d34..9ad8438c312a1e 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -84,180 +84,174 @@ function createConfigManager(configPath: string) { } describe('Server logging configuration', function() { - let child: Child.ChildProcess; + let child: undefined | Child.ChildProcess; + beforeEach(() => { Fs.mkdirSync(tempDir, { recursive: true }); }); afterEach(async () => { if (child !== undefined) { - child.kill(); - // wait for child to be killed otherwise jest complains that process not finished - await new Promise(res => setTimeout(res, 1000)); + const exitPromise = new Promise(resolve => child?.once('exit', resolve)); + child.kill('SIGKILL'); + await exitPromise; } + Del.sync(tempDir, { force: true }); }); - const isWindows = /^win/.test(process.platform); - if (isWindows) { + if (process.platform.startsWith('win')) { it('SIGHUP is not a feature of Windows.', () => { // nothing to do for Windows }); - } else { - describe('legacy logging', () => { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(legacyConfig, configFilePath); - - child = Child.spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - configFilePath, - '--verbose', - ]); - - const message$ = Rx.fromEvent(child.stdout, 'data').pipe( - map(messages => - String(messages) - .split('\n') - .filter(Boolean) - ) - ); - - await message$ - .pipe( - // We know the sighup handler will be registered before this message logged - filter(messages => messages.some(m => m.includes('setting up root'))), - take(1) - ) - .toPromise(); - - const lastMessage = await message$.pipe(take(1)).toPromise(); - expect(containsJsonOnly(lastMessage)).toBe(true); - - createConfigManager(configFilePath).modify(oldConfig => { - oldConfig.logging.json = false; - return oldConfig; - }); - - child.kill('SIGHUP'); - - await message$ - .pipe( - filter(messages => !containsJsonOnly(messages)), - take(1) - ) - .toPromise(); - }, - minute - ); - - it( - 'should recreate file handle on SIGHUP', - async function() { - const logPath = Path.resolve(tempDir, 'kibana.log'); - const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); - - child = Child.spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - legacyConfig, - '--logging.dest', - logPath, - '--verbose', - ]); - - await watchFileUntil(logPath, /setting up root/, 30 * second); - // once the server is running, archive the log file and issue SIGHUP - Fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - - await watchFileUntil( - logPath, - /Reloaded logging configuration due to SIGHUP/, - 30 * second - ); - }, - minute - ); - }); - - describe('platform logging', () => { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(configFileLogConsole, configFilePath); - - child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); - - const message$ = Rx.fromEvent(child.stdout, 'data').pipe( - map(messages => - String(messages) - .split('\n') - .filter(Boolean) - ) - ); - - await message$ - .pipe( - // We know the sighup handler will be registered before this message logged - filter(messages => messages.some(m => m.includes('setting up root'))), - take(1) - ) - .toPromise(); - - const lastMessage = await message$.pipe(take(1)).toPromise(); - expect(containsJsonOnly(lastMessage)).toBe(true); - - createConfigManager(configFilePath).modify(oldConfig => { - oldConfig.logging.appenders.console.layout.kind = 'pattern'; - return oldConfig; - }); - child.kill('SIGHUP'); - - await message$ - .pipe( - filter(messages => !containsJsonOnly(messages)), - take(1) - ) - .toPromise(); - }, - 30 * second - ); - it( - 'should recreate file handle on SIGHUP', - async function() { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(configFileLogFile, configFilePath); - - const logPath = Path.resolve(tempDir, 'kibana.log'); - const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); - - createConfigManager(configFilePath).modify(oldConfig => { - oldConfig.logging.appenders.file.path = logPath; - return oldConfig; - }); - - child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); - - await watchFileUntil(logPath, /setting up root/, 30 * second); - // once the server is running, archive the log file and issue SIGHUP - Fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - - await watchFileUntil( - logPath, - /Reloaded logging configuration due to SIGHUP/, - 30 * second - ); - }, - minute - ); - }); + return; } + + describe('legacy logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(legacyConfig, configFilePath); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + configFilePath, + '--verbose', + ]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.json = false; + return oldConfig; + }); + + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + minute + ); + + it( + 'should recreate file handle on SIGHUP', + async function() { + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + legacyConfig, + '--logging.dest', + logPath, + '--verbose', + ]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 30 * second); + }, + minute + ); + }); + + describe('platform logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogConsole, configFilePath); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.console.layout.kind = 'pattern'; + return oldConfig; + }); + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + 30 * second + ); + it( + 'should recreate file handle on SIGHUP', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogFile, configFilePath); + + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.file.path = logPath; + return oldConfig; + }); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 30 * second); + }, + minute + ); + }); }); diff --git a/src/dev/npm/integration_tests/installed_packages.test.ts b/src/dev/npm/integration_tests/installed_packages.test.ts index 5c942005d2eee3..75cd0e56076982 100644 --- a/src/dev/npm/integration_tests/installed_packages.test.ts +++ b/src/dev/npm/integration_tests/installed_packages.test.ts @@ -41,7 +41,7 @@ describe('src/dev/npm/installed_packages', () => { includeDev: true, }), ]); - }, 60 * 1000); + }, 360 * 1000); it('reads all installed packages of a module', () => { expect(fixture1Packages).toEqual([ diff --git a/tasks/test_jest.js b/tasks/test_jest.js index ff1a941610ad9d..bcb05a83675e5e 100644 --- a/tasks/test_jest.js +++ b/tasks/test_jest.js @@ -33,7 +33,7 @@ module.exports = function(grunt) { function runJest(jestScript) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci'], + args: [jestScript, '--ci', '--detectOpenHandles'], opts: { stdio: 'inherit' }, }; diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 3d30496ecb5822..b629e064b39b5e 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -11,7 +11,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> Running jest tests" cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose + checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose --detectOpenHandles echo "" echo "" From 2a03dffdad6c1dbeaf9a541c4ea0fb84183a8ca8 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Thu, 27 Feb 2020 22:45:53 +0100 Subject: [PATCH 14/34] [SIEM] Fix Timeline registerProvider to be called only when it's needed (#58051) --- .../drag_and_drop/draggable_wrapper.test.tsx | 31 +++++- .../drag_and_drop/draggable_wrapper.tsx | 99 +++++++++++-------- 2 files changed, 90 insertions(+), 40 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 92adc1a9adb7a2..d34f4cce9fea41 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -12,7 +12,7 @@ import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; -import { DraggableWrapper } from './draggable_wrapper'; +import { DraggableWrapper, ConditionalPortal } from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; describe('DraggableWrapper', () => { @@ -84,3 +84,32 @@ describe('DraggableWrapper', () => { }); }); }); + +describe('ConditionalPortal', () => { + const mount = useMountAppended(); + const props = { + usePortal: false, + registerProvider: jest.fn(), + isDragging: true, + }; + + it(`doesn't call registerProvider is NOT isDragging`, () => { + mount( + +
+ + + + + @@ -3407,7 +3371,7 @@ tr:hover .c3:focus::before {
- - - -
- - -
- - - - + - - -
- - - - - - + - + title="sha1: fa5195a..." + > - - sha1: fa5195a... - - + + - - - - - + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + + - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
+ + + + + + + + +
+ + + + + + + + + + + + + @@ -3709,7 +3655,7 @@ tr:hover .c3:focus::before {
- - - -
- - -
- - - - + - - -
- - - - - - + - + title="md5: f7653f1..." + > - - md5: f7653f1... - - + + - - - - - + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + + - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
+ + + + + + + + +
+ + + + + + + + + + + + + From b6dfcc7ad790cd85dd0071124dc23203281f3f87 Mon Sep 17 00:00:00 2001 From: igoristic Date: Thu, 27 Feb 2020 21:46:45 -0500 Subject: [PATCH 21/34] Removed unused indices (#57903) Co-authored-by: Elastic Machine --- .../plugins/monitoring/common/constants.ts | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 1fb6acdb915b88..9a4030f3eb2147 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -141,23 +141,12 @@ export const CLUSTER_ALERTS_ADDRESS_CONFIG_KEY = 'cluster_alerts.email_notificat export const STANDALONE_CLUSTER_CLUSTER_UUID = '__standalone_cluster__'; -const INDEX_PATTERN_NEW = ',monitoring-*-7-*,monitoring-*-8-*'; -const INDEX_PATTERN_KIBANA_NEW = ',monitoring-kibana-7-*,monitoring-kibana-8-*'; -const INDEX_PATTERN_LOGSTASH_NEW = ',monitoring-logstash-7-*,monitoring-logstash-8-*'; -const INDEX_PATTERN_BEATS_NEW = ',monitoring-beats-7-*,monitoring-beats-8-*'; -const INDEX_ALERTS_NEW = ',monitoring-alerts-7,monitoring-alerts-8'; -const INDEX_PATTERN_ELASTICSEARCH_NEW = ',monitoring-es-7-*,monitoring-es-8-*'; - -export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*' + INDEX_PATTERN_NEW; -export const INDEX_PATTERN_KIBANA = - '.monitoring-kibana-6-*,.monitoring-kibana-7-*' + INDEX_PATTERN_KIBANA_NEW; -export const INDEX_PATTERN_LOGSTASH = - '.monitoring-logstash-6-*,.monitoring-logstash-7-*' + INDEX_PATTERN_LOGSTASH_NEW; -export const INDEX_PATTERN_BEATS = - '.monitoring-beats-6-*,.monitoring-beats-7-*' + INDEX_PATTERN_BEATS_NEW; -export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7' + INDEX_ALERTS_NEW; -export const INDEX_PATTERN_ELASTICSEARCH = - '.monitoring-es-6-*,.monitoring-es-7-*' + INDEX_PATTERN_ELASTICSEARCH_NEW; +export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*'; +export const INDEX_PATTERN_KIBANA = '.monitoring-kibana-6-*,.monitoring-kibana-7-*'; +export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logstash-7-*'; +export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; +export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7'; +export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; // This is the unique token that exists in monitoring indices collected by metricbeat export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; From 4fd3b45b44fb213c3c1251d3ccbcb471a434caf9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 27 Feb 2020 20:22:42 -0700 Subject: [PATCH 22/34] fix some missing awaits in functional tests (#58807) --- test/functional/apps/context/_filters.js | 2 +- test/functional/apps/dashboard/panel_expand_toggle.js | 2 +- test/functional/apps/visualize/_tsvb_markdown.ts | 4 ++-- x-pack/test/functional/page_objects/uptime_page.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index c9499f5a805ab3..721f8a50d0e464 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -64,7 +64,7 @@ export default function({ getService, getPageObjects }) { await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - retry.try(async () => { + await retry.try(async () => { expect( await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false) ).to.be(true); diff --git a/test/functional/apps/dashboard/panel_expand_toggle.js b/test/functional/apps/dashboard/panel_expand_toggle.js index 930445a67aa203..5e7d55706968de 100644 --- a/test/functional/apps/dashboard/panel_expand_toggle.js +++ b/test/functional/apps/dashboard/panel_expand_toggle.js @@ -56,7 +56,7 @@ export default function({ getService, getPageObjects }) { // Add a retry to fix https://github.com/elastic/kibana/issues/14574. Perhaps the recent changes to this // being a CSS update is causing the UI to change slower than grabbing the panels? - retry.try(async () => { + await retry.try(async () => { const panelCountAfterMaxThenMinimize = await PageObjects.dashboard.getPanelCount(); expect(panelCountAfterMaxThenMinimize).to.be(panelCount); }); diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index b7307ac9c6cabc..d37404a3d60cb2 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.markdownSwitchSubTab('data'); await visualBuilder.cloneSeries(); - retry.try(async function seriesCountCheck() { + await retry.try(async function seriesCountCheck() { const seriesLength = (await visualBuilder.getSeries()).length; expect(seriesLength).to.be.equal(2); }); @@ -131,7 +131,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.markdownSwitchSubTab('data'); await visualBuilder.createNewAgg(); - retry.try(async function aggregationCountCheck() { + await retry.try(async function aggregationCountCheck() { const aggregationLength = await visualBuilder.getAggregationCount(); expect(aggregationLength).to.be.equal(2); }); diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index a5bd4cc4802873..f6e93cd14e4971 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -57,7 +57,7 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo } public async pageUrlContains(value: string, expected: boolean = true) { - retry.try(async () => { + await retry.try(async () => { expect(await uptimeService.urlContains(value)).to.eql(expected); }); } From 015c7abbca81887d6d2d18c3dc44613d8f9c4c71 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 27 Feb 2020 21:49:33 -0700 Subject: [PATCH 23/34] [Maps] remove getMetricFields from AbstractESSource (#58667) * [Maps] remove getMetricFields from AbstractESSource * do not use formaters for count and unique count * fix jest test --- .../maps/public/layers/fields/es_agg_field.js | 21 +++++++--- .../maps/public/layers/fields/field.js | 8 ++++ .../public/layers/sources/es_agg_source.js | 17 ++++++-- .../maps/public/layers/sources/es_source.js | 42 ++++--------------- .../public/layers/sources/es_term_source.js | 22 ++++------ .../layers/sources/es_term_source.test.js | 2 +- .../maps/public/layers/sources/source.js | 4 +- .../properties/dynamic_color_property.test.js | 3 ++ .../properties/dynamic_style_property.js | 20 +++------ .../tooltips/es_aggmetric_tooltip_property.js | 4 +- .../maps/public/layers/vector_layer.js | 11 +++-- 11 files changed, 72 insertions(+), 82 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js index 28c199b64d3ef4..27ab8fc5bfb3ae 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -21,13 +21,17 @@ export class ESAggMetricField extends AbstractField { } getName() { - return this._source.formatMetricKey(this.getAggType(), this.getESDocFieldName()); + return this._source.getAggKey(this.getAggType(), this.getRootName()); + } + + getRootName() { + return this._getESDocFieldName(); } async getLabel() { return this._label - ? await this._label - : this._source.formatMetricLabel(this.getAggType(), this.getESDocFieldName()); + ? this._label + : this._source.getAggLabel(this.getAggType(), this.getRootName()); } getAggType() { @@ -42,13 +46,13 @@ export class ESAggMetricField extends AbstractField { return this.getAggType() === AGG_TYPE.TERMS ? 'string' : 'number'; } - getESDocFieldName() { + _getESDocFieldName() { return this._esDocField ? this._esDocField.getName() : ''; } getRequestDescription() { return this.getAggType() !== AGG_TYPE.COUNT - ? `${this.getAggType()} ${this.getESDocFieldName()}` + ? `${this.getAggType()} ${this.getRootName()}` : AGG_TYPE.COUNT; } @@ -64,7 +68,7 @@ export class ESAggMetricField extends AbstractField { } getValueAggDsl(indexPattern) { - const field = getField(indexPattern, this.getESDocFieldName()); + const field = getField(indexPattern, this.getRootName()); const aggType = this.getAggType(); const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: 1 } : {}; return { @@ -77,6 +81,11 @@ export class ESAggMetricField extends AbstractField { return !isMetricCountable(this.getAggType()); } + canValueBeFormatted() { + // Do not use field formatters for counting metrics + return ![AGG_TYPE.COUNT, AGG_TYPE.UNIQUE_COUNT].includes(this.getAggType()); + } + async getOrdinalFieldMetaRequest(config) { return this._esDocField.getOrdinalFieldMetaRequest(config); } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js index b5d157ad1697aa..2dd553f66755fc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -17,6 +17,14 @@ export class AbstractField { return this._fieldName; } + getRootName() { + return this.getName(); + } + + canValueBeFormatted() { + return true; + } + getSource() { return this._source; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js index bee35216f59dab..775535d9e2299c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { AbstractESSource } from './es_source'; import { ESAggMetricField } from '../fields/es_agg_field'; import { ESDocField } from '../fields/es_doc_field'; @@ -72,12 +73,22 @@ export class AbstractESAggSource extends AbstractESSource { return metrics; } - formatMetricKey(aggType, fieldName) { + getAggKey(aggType, fieldName) { return aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME; } - formatMetricLabel(aggType, fieldName) { - return aggType !== AGG_TYPE.COUNT ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL; + getAggLabel(aggType, fieldName) { + switch (aggType) { + case AGG_TYPE.COUNT: + return COUNT_PROP_LABEL; + case AGG_TYPE.TERMS: + return i18n.translate('xpack.maps.source.esAggSource.topTermLabel', { + defaultMessage: `Top {fieldName}`, + values: { fieldName }, + }); + default: + return `${aggType} ${fieldName}`; + } } getValueAggsDsl(indexPattern) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 5074b218dd615a..f575fd05c80613 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -17,7 +17,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { copyPersistentState } from '../../reducers/util'; -import { ES_GEO_FIELD_TYPE, AGG_TYPE } from '../../../common/constants'; +import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; import { DataRequestAbortError } from '../util/data_request'; import { expandToTileBoundaries } from './es_geo_grid_source/geo_tile_utils'; @@ -72,10 +72,6 @@ export class AbstractESSource extends AbstractVectorSource { return clonedDescriptor; } - getMetricFields() { - return []; - } - async _runEsQuery({ requestId, requestName, @@ -254,23 +250,7 @@ export class AbstractESSource extends AbstractVectorSource { return this._descriptor.id; } - async getFieldFormatter(fieldName) { - const metricField = this.getMetricFields().find(field => field.getName() === fieldName); - - // Do not use field formatters for counting metrics - if ( - metricField && - (metricField.type === AGG_TYPE.COUNT || metricField.type === AGG_TYPE.UNIQUE_COUNT) - ) { - return null; - } - - // fieldName could be an aggregation so it needs to be unpacked to expose raw field. - const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName; - if (!realFieldName) { - return null; - } - + async createFieldFormatter(field) { let indexPattern; try { indexPattern = await this.getIndexPattern(); @@ -278,7 +258,7 @@ export class AbstractESSource extends AbstractVectorSource { return null; } - const fieldFromIndexPattern = indexPattern.fields.getByName(realFieldName); + const fieldFromIndexPattern = indexPattern.fields.getByName(field.getRootName()); if (!fieldFromIndexPattern) { return null; } @@ -336,25 +316,19 @@ export class AbstractESSource extends AbstractVectorSource { return resp.aggregations; } - getValueSuggestions = async (fieldName, query) => { - // fieldName could be an aggregation so it needs to be unpacked to expose raw field. - const metricField = this.getMetricFields().find(field => field.getName() === fieldName); - const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName; - if (!realFieldName) { - return []; - } - + getValueSuggestions = async (field, query) => { try { const indexPattern = await this.getIndexPattern(); - const field = indexPattern.fields.getByName(realFieldName); return await autocompleteService.getValueSuggestions({ indexPattern, - field, + field: indexPattern.fields.getByName(field.getRootName()), query, }); } catch (error) { console.warn( - `Unable to fetch suggestions for field: ${fieldName}, query: ${query}, error: ${error.message}` + `Unable to fetch suggestions for field: ${field.getRootName()}, query: ${query}, error: ${ + error.message + }` ); return []; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index 9cc2919404a940..30f60f543d38df 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -67,7 +67,7 @@ export class ESTermSource extends AbstractESAggSource { return this._descriptor.whereQuery; } - formatMetricKey(aggType, fieldName) { + getAggKey(aggType, fieldName) { const metricKey = aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : aggType; return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${ @@ -75,21 +75,13 @@ export class ESTermSource extends AbstractESAggSource { }.${this._termField.getName()}`; } - formatMetricLabel(type, fieldName) { - switch (type) { - case AGG_TYPE.COUNT: - return i18n.translate('xpack.maps.source.esJoin.countLabel', { + getAggLabel(aggType, fieldName) { + return aggType === AGG_TYPE.COUNT + ? i18n.translate('xpack.maps.source.esJoin.countLabel', { defaultMessage: `Count of {indexPatternTitle}`, values: { indexPatternTitle: this._descriptor.indexPatternTitle }, - }); - case AGG_TYPE.TERMS: - return i18n.translate('xpack.maps.source.esJoin.topTermLabel', { - defaultMessage: `Top {fieldName}`, - values: { fieldName }, - }); - default: - return `${type} ${fieldName}`; - } + }) + : super.getAggLabel(aggType, fieldName); } async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) { @@ -116,7 +108,7 @@ export class ESTermSource extends AbstractESAggSource { requestDescription: this._getRequestDescription(leftSourceName, leftFieldName), }); - const countPropertyName = this.formatMetricKey(AGG_TYPE.COUNT); + const countPropertyName = this.getAggKey(AGG_TYPE.COUNT); return { propertiesMap: extractPropertiesMap(rawEsData, countPropertyName), }; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js index 39cc301d458cbe..d6f9f6d2911e91 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js @@ -54,7 +54,7 @@ describe('getMetricFields', () => { expect(metrics.length).toBe(2); expect(metrics[0].getAggType()).toEqual('sum'); - expect(metrics[0].getESDocFieldName()).toEqual(sumFieldName); + expect(metrics[0].getRootName()).toEqual(sumFieldName); expect(metrics[0].getName()).toEqual( '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField' ); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index 3c6ddb74bedeba..4fef52e731f9b2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -132,7 +132,7 @@ export class AbstractSource { } // Returns function used to format value - async getFieldFormatter(/* fieldName */) { + async createFieldFormatter(/* field */) { return null; } @@ -140,7 +140,7 @@ export class AbstractSource { throw new Error(`Source#loadStylePropsMeta not implemented`); } - async getValueSuggestions(/* fieldName, query */) { + async getValueSuggestions(/* field, query */) { return []; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index 6b08fc2a105c3a..8648b073a7b79a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -25,6 +25,9 @@ const mockField = { getName() { return 'foobar'; }, + getRootName() { + return 'foobar'; + }, supportsFieldMeta() { return true; }, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index af78c4c0e461e8..e40c82e6276c7e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -13,7 +13,6 @@ import React from 'react'; import { OrdinalLegend } from './components/ordinal_legend'; import { CategoricalLegend } from './components/categorical_legend'; import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta_options_popover'; -import { ESAggMetricField } from '../../../fields/es_agg_field'; export class DynamicStyleProperty extends AbstractStyleProperty { static type = STYLE_TYPE.DYNAMIC; @@ -26,9 +25,9 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } getValueSuggestions = query => { - const fieldName = this.getFieldName(); + const field = this.getField(); const fieldSource = this.getFieldSource(); - return fieldSource && fieldName ? fieldSource.getValueSuggestions(fieldName, query) : []; + return fieldSource && field ? fieldSource.getValueSuggestions(field, query) : []; }; getFieldMeta() { @@ -185,11 +184,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } _pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData) { - const realFieldName = - this._field instanceof ESAggMetricField - ? this._field.getESDocFieldName() - : this._field.getName(); - const stats = fieldMetaData[realFieldName]; + const stats = fieldMetaData[this._field.getRootName()]; if (!stats) { return null; } @@ -209,15 +204,12 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } _pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData) { - const realFieldName = - this._field instanceof ESAggMetricField - ? this._field.getESDocFieldName() - : this._field.getName(); - if (!fieldMetaData[realFieldName] || !fieldMetaData[realFieldName].buckets) { + const rootFieldName = this._field.getRootName(); + if (!fieldMetaData[rootFieldName] || !fieldMetaData[rootFieldName].buckets) { return null; } - const ordered = fieldMetaData[realFieldName].buckets.map(bucket => { + const ordered = fieldMetaData[rootFieldName].buckets.map(bucket => { return { key: bucket.key, count: bucket.doc_count, diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js index 229c84fe234bd9..ea000a78331eb7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js @@ -27,9 +27,7 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty { ) { return this._rawValue; } - const indexPatternField = this._indexPattern.fields.getByName( - this._metricField.getESDocFieldName() - ); + const indexPatternField = this._indexPattern.fields.getByName(this._metricField.getRootName()); if (!indexPatternField) { return this._rawValue; } 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 e1a30c8aef1d37..c515feecc15513 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -561,10 +561,13 @@ export class VectorLayer extends AbstractLayer { startLoading(dataRequestId, requestToken, nextMeta); const formatters = {}; - const promises = fields.map(async field => { - const fieldName = field.getName(); - formatters[fieldName] = await source.getFieldFormatter(fieldName); - }); + const promises = fields + .filter(field => { + return field.canValueBeFormatted(); + }) + .map(async field => { + formatters[field.getName()] = await source.createFieldFormatter(field); + }); await Promise.all(promises); stopLoading(dataRequestId, requestToken, formatters, nextMeta); From 511a9c2bee83890a874c124f30231482e6a5773e Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 27 Feb 2020 22:18:00 -0700 Subject: [PATCH 24/34] Revert "Prep agg types for new platform (#57064)" This reverts commit 0cede6a705db65ef450426db3c584fcabab42780. --- packages/kbn-utility-types/README.md | 1 - packages/kbn-utility-types/index.ts | 2 +- .../filters/brush_event.test.mocks.ts} | 13 +- .../actions/filters/brush_event.test.ts | 49 +- src/legacy/core_plugins/data/public/index.ts | 12 +- src/legacy/core_plugins/data/public/plugin.ts | 6 +- .../public/search/aggs/agg_config.test.ts | 497 ------ .../data/public/search/aggs/agg_config.ts | 61 +- .../public/search/aggs/agg_configs.test.ts | 503 ------ .../data/public/search/aggs/agg_configs.ts | 76 +- .../public/search/aggs/agg_params.test.ts | 2 + .../data/public/search/aggs/agg_type.test.ts | 16 +- .../data/public/search/aggs/agg_type.ts | 5 +- .../search/aggs/agg_types_registry.test.ts | 91 -- .../public/search/aggs/agg_types_registry.ts | 68 - .../search/aggs/buckets/_bucket_agg_type.ts | 12 +- .../search/aggs/buckets/_interval_options.ts | 1 - .../create_filter/date_histogram.test.ts | 12 +- .../buckets/create_filter/date_range.test.ts | 7 +- .../buckets/create_filter/filters.test.ts | 13 +- .../buckets/create_filter/histogram.test.ts | 12 +- .../buckets/create_filter/ip_range.test.ts | 11 +- .../aggs/buckets/create_filter/range.test.ts | 12 +- .../aggs/buckets/create_filter/terms.test.ts | 11 +- .../search/aggs/buckets/date_histogram.ts | 8 +- .../search/aggs/buckets/date_range.test.ts | 25 +- .../public/search/aggs/buckets/date_range.ts | 12 +- .../data/public/search/aggs/buckets/filter.ts | 1 - .../public/search/aggs/buckets/filters.ts | 26 +- .../search/aggs/buckets/geo_hash.test.ts | 7 +- .../public/search/aggs/buckets/geo_tile.ts | 3 +- .../search/aggs/buckets/histogram.test.ts | 33 +- .../public/search/aggs/buckets/histogram.ts | 12 +- .../public/search/aggs/buckets/ip_range.ts | 12 +- .../buckets/migrate_include_exclude_format.ts | 4 +- .../public/search/aggs/buckets/range.test.ts | 12 +- .../aggs/buckets/significant_terms.test.ts | 9 +- .../public/search/aggs/buckets/terms.test.ts | 8 +- .../aggs/filter/agg_type_filters.test.ts | 5 +- .../search/aggs/filter/agg_type_filters.ts | 1 - .../search/aggs/filter/prop_filter.test.ts | 19 +- .../data/public/search/aggs/index.test.ts | 2 + .../data/public/search/aggs/index.ts | 9 +- .../public/search/aggs/metrics/bucket_avg.ts | 1 + .../public/search/aggs/metrics/bucket_max.ts | 1 + .../public/search/aggs/metrics/bucket_min.ts | 1 - .../public/search/aggs/metrics/cardinality.ts | 5 +- .../data/public/search/aggs/metrics/count.ts | 7 +- .../lib/get_response_agg_config_class.ts | 1 - .../metrics/lib/parent_pipeline_agg_helper.ts | 1 + .../lib/sibling_pipeline_agg_helper.ts | 1 + .../public/search/aggs/metrics/median.test.ts | 7 +- .../data/public/search/aggs/metrics/median.ts | 4 +- .../search/aggs/metrics/metric_agg_type.ts | 7 +- .../data/public/search/aggs/metrics/min.ts | 1 - .../aggs/metrics/parent_pipeline.test.ts | 18 +- .../aggs/metrics/percentile_ranks.test.ts | 8 +- .../search/aggs/metrics/percentile_ranks.ts | 7 +- .../search/aggs/metrics/percentiles.test.ts | 6 +- .../public/search/aggs/metrics/percentiles.ts | 4 + .../aggs/metrics/sibling_pipeline.test.ts | 22 +- .../search/aggs/metrics/std_deviation.test.ts | 6 +- .../search/aggs/metrics/top_hit.test.ts | 6 +- .../public/search/aggs/param_types/agg.ts | 4 +- .../public/search/aggs/param_types/base.ts | 4 +- .../search/aggs/param_types/field.test.ts | 2 + .../public/search/aggs/param_types/field.ts | 5 +- .../param_types/filter/field_filters.test.ts | 11 +- .../aggs/param_types/filter/field_filters.ts | 8 +- .../search/aggs/param_types/json.test.ts | 8 +- .../public/search/aggs/param_types/json.ts | 4 +- .../search/aggs/param_types/optioned.test.ts | 2 + .../search/aggs/param_types/optioned.ts | 6 +- .../search/aggs/param_types/string.test.ts | 8 +- .../public/search/aggs/param_types/string.ts | 4 +- .../test_helpers/mock_agg_types_registry.ts | 57 - .../aggs/test_helpers/mock_data_services.ts | 54 - .../data/public/search/aggs/types.ts | 2 +- .../data/public/search/aggs/utils.test.tsx | 2 + .../data/public/search/aggs/utils.ts | 39 +- .../data/public/search/expressions/esaggs.ts | 4 +- .../data/public/search/expressions/utils.ts | 5 +- .../core_plugins/data/public/search/mocks.ts | 85 - .../data/public/search/search_service.ts | 60 +- .../data/public/search/tabify/buckets.test.ts | 2 + .../public/search/tabify/get_columns.test.ts | 22 +- .../search/tabify/response_writer.test.ts | 20 +- .../data/public/search/tabify/tabify.test.ts | 16 +- .../components/sidebar/state/reducers.ts | 18 +- .../public/legacy_imports.ts | 2 +- .../public/table_vis_controller.test.ts | 4 +- .../visualizations/public/legacy_imports.ts | 2 +- .../public/np_ready/public/vis_impl.js | 6 +- src/legacy/ui/public/agg_types/index.ts | 9 +- .../ui/public/vis/__tests__/_agg_config.js | 485 ++++++ .../ui/public/vis/__tests__/_agg_configs.js | 420 +++++ .../public/vis/__tests__/index.js} | 4 +- .../data/common/field_formats/mocks.ts | 49 - src/plugins/data/public/mocks.ts | 28 +- .../data/public/search/search_source/mocks.ts | 19 + .../editor_frame_service/service.test.tsx | 4 + .../__snapshots__/zeek_details.test.tsx.snap | 1448 +++++++++-------- 102 files changed, 2101 insertions(+), 2646 deletions(-) rename src/legacy/core_plugins/data/public/{services.ts => actions/filters/brush_event.test.mocks.ts} (76%) delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts delete mode 100644 src/legacy/core_plugins/data/public/search/mocks.ts create mode 100644 src/legacy/ui/public/vis/__tests__/_agg_config.js create mode 100644 src/legacy/ui/public/vis/__tests__/_agg_configs.js rename src/legacy/{core_plugins/data/public/search/aggs/test_helpers/index.ts => ui/public/vis/__tests__/index.js} (86%) delete mode 100644 src/plugins/data/common/field_formats/mocks.ts diff --git a/packages/kbn-utility-types/README.md b/packages/kbn-utility-types/README.md index b57e98e379707e..829fd21e143669 100644 --- a/packages/kbn-utility-types/README.md +++ b/packages/kbn-utility-types/README.md @@ -18,7 +18,6 @@ type B = UnwrapPromise
; // string ## Reference -- `Assign` — From `U` assign properties to `T` (just like object assign). - `Ensure` — Makes sure `T` is of type `X`. - `ObservableLike` — Minimal interface for an object resembling an `Observable`. - `PublicContract` — Returns an object with public keys only. diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 657d9f547de66e..808935ed4cb5b8 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -18,7 +18,7 @@ */ import { PromiseType } from 'utility-types'; -export { $Values, Assign, Class, Optional, Required } from 'utility-types'; +export { $Values, Required, Optional, Class } from 'utility-types'; /** * A type that may or may not be a `Promise`. diff --git a/src/legacy/core_plugins/data/public/services.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts similarity index 76% rename from src/legacy/core_plugins/data/public/services.ts rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts index 7ecd041c70e22e..2cecfd0fe8b767 100644 --- a/src/legacy/core_plugins/data/public/services.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts @@ -17,9 +17,12 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { SearchStart } from './search/search_service'; +import { chromeServiceMock } from '../../../../../../core/public/mocks'; -export const [getSearchServiceShim, setSearchServiceShim] = createGetterSetter( - 'searchShim' -); +jest.doMock('ui/new_platform', () => ({ + npStart: { + core: { + chrome: chromeServiceMock.createStartContract(), + }, + }, +})); diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts index eb29530f92fee7..0e18c7c707fa3f 100644 --- a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts @@ -19,13 +19,33 @@ import moment from 'moment'; -import { onBrushEvent, BrushEvent } from './brush_event'; +jest.mock('../../search/aggs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }: Record) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + +jest.mock('../../../../../../plugins/data/public/services', () => ({ + getIndexPatterns: () => { + return { + get: async () => { + return { + id: 'logstash-*', + timeFieldName: 'time', + }; + }, + }; + }, +})); -import { mockDataServices } from '../../search/aggs/test_helpers'; -import { IndexPatternsContract } from '../../../../../../plugins/data/public'; -import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setIndexPatterns } from '../../../../../../plugins/data/public/services'; +import { onBrushEvent, BrushEvent } from './brush_event'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; @@ -39,28 +59,11 @@ describe('brushEvent', () => { }, getIndexPattern: () => ({ timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, }), }, ]; beforeEach(() => { - mockDataServices(); - setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, - get: async () => ({ - id: 'indexPatternId', - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), - } as unknown) as IndexPatternsContract); - baseEvent = { data: { ordered: { diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 8d730d18a17559..8cde5d0a1fc115 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -35,18 +35,18 @@ export { } from '../../../../plugins/data/public'; export { // agg_types - AggParam, // only the type is used externally, only in vis editor - AggParamOption, // only the type is used externally - DateRangeKey, // only used in field formatter deserialization, which will live in data + AggParam, + AggParamOption, + DateRangeKey, IAggConfig, IAggConfigs, IAggType, IFieldParamType, IMetricAggType, - IpRangeKey, // only used in field formatter deserialization, which will live in data + IpRangeKey, ISchemas, - OptionedParamEditorProps, // only type is used externally - OptionedValueProp, // only type is used externally + OptionedParamEditorProps, + OptionedValueProp, } from './search/types'; /** @public static code */ diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index e2b8ca5dda78cf..e13e8e34eaebec 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -36,7 +36,6 @@ import { setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; -import { setSearchServiceShim } from './services'; import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; import { @@ -113,9 +112,6 @@ export class DataPlugin } public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { - const search = this.search.start(core); - setSearchServiceShim(search); - setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); @@ -127,7 +123,7 @@ export class DataPlugin uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return { - search, + search: this.search.start(core), }; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts deleted file mode 100644 index 7769aa29184d3a..00000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts +++ /dev/null @@ -1,497 +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 { identity } from 'lodash'; - -import { AggConfig, IAggConfig } from './agg_config'; -import { AggConfigs, CreateAggConfigParams } from './agg_configs'; -import { AggType } from './agg_types'; -import { AggTypesRegistryStart } from './agg_types_registry'; -import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; -import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { stubIndexPatternWithFields } from '../../../../../../plugins/data/public/stubs'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setFieldFormats } from '../../../../../../plugins/data/public/services'; - -describe('AggConfig', () => { - let indexPattern: IndexPattern; - let typesRegistry: AggTypesRegistryStart; - - beforeEach(() => { - jest.restoreAllMocks(); - mockDataServices(); - indexPattern = stubIndexPatternWithFields as IndexPattern; - typesRegistry = mockAggTypesRegistry(); - }); - - describe('#toDsl', () => { - it('calls #write()', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - const configStates = { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }; - const aggConfig = ac.createAggConfig(configStates); - - const spy = jest.spyOn(aggConfig, 'write').mockImplementation(() => ({ params: {} })); - aggConfig.toDsl(); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('uses the type name as the agg name', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - const configStates = { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }; - const aggConfig = ac.createAggConfig(configStates); - - jest.spyOn(aggConfig, 'write').mockImplementation(() => ({ params: {} })); - const dsl = aggConfig.toDsl(); - expect(dsl).toHaveProperty('date_histogram'); - }); - - it('uses the params from #write() output as the agg params', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - const configStates = { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }; - const aggConfig = ac.createAggConfig(configStates); - - const football = {}; - jest.spyOn(aggConfig, 'write').mockImplementation(() => ({ params: football })); - const dsl = aggConfig.toDsl(); - expect(dsl.date_histogram).toBe(football); - }); - - it('includes subAggs from #write() output', () => { - const configStates = [ - { - enabled: true, - type: 'avg', - schema: 'metric', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - - const histoConfig = ac.byName('date_histogram')[0]; - const avgConfig = ac.byName('avg')[0]; - const football = {}; - - jest - .spyOn(histoConfig, 'write') - .mockImplementation(() => ({ params: {}, subAggs: [avgConfig] })); - jest.spyOn(avgConfig, 'write').mockImplementation(() => ({ params: football })); - - const dsl = histoConfig.toDsl(); - expect(dsl).toHaveProperty('aggs'); - expect(dsl.aggs).toHaveProperty(avgConfig.id); - expect(dsl.aggs[avgConfig.id]).toHaveProperty('avg'); - expect(dsl.aggs[avgConfig.id].avg).toBe(football); - }); - }); - - describe('::ensureIds', () => { - it('accepts an array of objects and assigns ids to them', () => { - const objs = [{}, {}, {}, {}]; - AggConfig.ensureIds(objs); - expect(objs[0]).toHaveProperty('id', '1'); - expect(objs[1]).toHaveProperty('id', '2'); - expect(objs[2]).toHaveProperty('id', '3'); - expect(objs[3]).toHaveProperty('id', '4'); - }); - - it('assigns ids relative to the other only item in the list', () => { - const objs = [{ id: '100' }, {}]; - AggConfig.ensureIds(objs); - expect(objs[0]).toHaveProperty('id', '100'); - expect(objs[1]).toHaveProperty('id', '101'); - }); - - it('assigns ids relative to the other items in the list', () => { - const objs = [{ id: '100' }, { id: '200' }, { id: '500' }, { id: '350' }, {}]; - AggConfig.ensureIds(objs); - expect(objs[0]).toHaveProperty('id', '100'); - expect(objs[1]).toHaveProperty('id', '200'); - expect(objs[2]).toHaveProperty('id', '500'); - expect(objs[3]).toHaveProperty('id', '350'); - expect(objs[4]).toHaveProperty('id', '501'); - }); - - it('uses ::nextId to get the starting value', () => { - jest.spyOn(AggConfig, 'nextId').mockImplementation(() => 534); - const objs = AggConfig.ensureIds([{}]); - expect(objs[0]).toHaveProperty('id', '534'); - }); - - it('only calls ::nextId once', () => { - const start = 420; - const spy = jest.spyOn(AggConfig, 'nextId').mockImplementation(() => start); - const objs = AggConfig.ensureIds([{}, {}, {}, {}, {}, {}, {}]); - - expect(spy).toHaveBeenCalledTimes(1); - objs.forEach((obj, i) => { - expect(obj).toHaveProperty('id', String(start + i)); - }); - }); - }); - - describe('::nextId', () => { - it('accepts a list of objects and picks the next id', () => { - const next = AggConfig.nextId([{ id: '100' }, { id: '500' }] as IAggConfig[]); - expect(next).toBe(501); - }); - - it('handles an empty list', () => { - const next = AggConfig.nextId([]); - expect(next).toBe(1); - }); - - it('fails when the list is not defined', () => { - expect(() => { - AggConfig.nextId((undefined as unknown) as IAggConfig[]); - }).toThrowError(); - }); - }); - - describe('#toJsonDataEquals', () => { - const testsIdentical = [ - [ - { - enabled: true, - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - ], - [ - { - enabled: true, - type: 'avg', - schema: 'metric', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ], - ]; - - testsIdentical.forEach((configState, index) => { - it(`identical aggregations (${index})`, () => { - const ac1 = new AggConfigs(indexPattern, configState, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, configState, { typesRegistry }); - expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); - }); - }); - - const testsIdenticalDifferentOrder = [ - { - config1: [ - { - enabled: true, - type: 'avg', - schema: 'metric', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ], - config2: [ - { - enabled: true, - schema: 'metric', - type: 'avg', - params: {}, - }, - { - enabled: true, - schema: 'segment', - type: 'date_histogram', - params: {}, - }, - ], - }, - ]; - - testsIdenticalDifferentOrder.forEach((test, index) => { - it(`identical aggregations (${index}) - init json is in different order`, () => { - const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); - expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); - }); - }); - - const testsDifferent = [ - { - config1: [ - { - enabled: true, - type: 'avg', - schema: 'metric', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ], - config2: [ - { - enabled: true, - type: 'max', - schema: 'metric', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ], - }, - { - config1: [ - { - enabled: true, - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - ], - config2: [ - { - enabled: true, - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ], - }, - ]; - - testsDifferent.forEach((test, index) => { - it(`different aggregations (${index})`, () => { - const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); - expect(ac1.jsonDataEquals(ac2.aggs)).toBe(false); - }); - }); - }); - - describe('#toJSON', () => { - it('includes the aggs id, params, type and schema', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - const configStates = { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }; - const aggConfig = ac.createAggConfig(configStates); - - expect(aggConfig.id).toBe('1'); - expect(typeof aggConfig.params).toBe('object'); - expect(aggConfig.type).toBeInstanceOf(AggType); - expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); - expect(typeof aggConfig.schema).toBe('object'); - expect(aggConfig.schema).toHaveProperty('name', 'segment'); - - const state = aggConfig.toJSON(); - expect(state).toHaveProperty('id', '1'); - expect(typeof state.params).toBe('object'); - expect(state).toHaveProperty('type', 'date_histogram'); - expect(state).toHaveProperty('schema', 'segment'); - }); - - it('test serialization order is identical (for visual consistency)', () => { - const configStates = [ - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: {}, - }, - ]; - const ac1 = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, configStates, { typesRegistry }); - - // this relies on the assumption that js-engines consistently loop over properties in insertion order. - // most likely the case, but strictly speaking not guaranteed by the JS and JSON specifications. - expect(JSON.stringify(ac1.aggs) === JSON.stringify(ac2.aggs)).toBe(true); - }); - }); - - describe('#makeLabel', () => { - let aggConfig: AggConfig; - - beforeEach(() => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - aggConfig = ac.createAggConfig({ type: 'count' } as CreateAggConfigParams); - }); - - it('uses the custom label if it is defined', () => { - aggConfig.params.customLabel = 'Custom label'; - const label = aggConfig.makeLabel(); - expect(label).toBe(aggConfig.params.customLabel); - }); - - it('default label should be "Count"', () => { - const label = aggConfig.makeLabel(); - expect(label).toBe('Count'); - }); - - it('default label should be "Percentage of Count" when percentageMode is set to true', () => { - const label = aggConfig.makeLabel(true); - expect(label).toBe('Percentage of Count'); - }); - - it('empty label if the type is not defined', () => { - aggConfig.type = (undefined as unknown) as AggType; - const label = aggConfig.makeLabel(); - expect(label).toBe(''); - }); - }); - - describe('#fieldFormatter - custom getFormat handler', () => { - it('returns formatter from getFormat handler', () => { - setFieldFormats({ - ...dataPluginMock.createStartContract().fieldFormats, - getDefaultInstance: jest.fn().mockImplementation(() => ({ - getConverterFor: jest.fn().mockImplementation(() => (t: string) => t), - })) as any, - }); - - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); - const configStates = { - enabled: true, - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }; - const aggConfig = ac.createAggConfig(configStates); - - const fieldFormatter = aggConfig.fieldFormatter(); - expect(fieldFormatter).toBeDefined(); - expect(fieldFormatter('text')).toBe('text'); - }); - }); - - // TODO: Converting these field formatter tests from browser tests to unit - // tests makes them much less helpful due to the extensive use of mocking. - // We should revisit these and rewrite them into something more useful. - describe('#fieldFormatter - no custom getFormat handler', () => { - let aggConfig: AggConfig; - - beforeEach(() => { - setFieldFormats({ - ...dataPluginMock.createStartContract().fieldFormats, - getDefaultInstance: jest.fn().mockImplementation(() => ({ - getConverterFor: (t?: string) => t || identity, - })) as any, - }); - indexPattern.fields.getByName = name => - ({ - format: { - getConverterFor: (t?: string) => t || identity, - }, - } as IndexPatternField); - - const configStates = { - enabled: true, - type: 'histogram', - schema: 'bucket', - params: { - field: { - format: { - getConverterFor: (t?: string) => t || identity, - }, - }, - }, - }; - const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry }); - aggConfig = ac.createAggConfig(configStates); - }); - - it("returns the field's formatter", () => { - expect(aggConfig.fieldFormatter().toString()).toBe( - aggConfig - .getField() - .format.getConverterFor() - .toString() - ); - }); - - it('returns the string format if the field does not have a format', () => { - const agg = aggConfig; - agg.params.field = { type: 'number', format: null }; - const fieldFormatter = agg.fieldFormatter(); - expect(fieldFormatter).toBeDefined(); - expect(fieldFormatter('text')).toBe('text'); - }); - - it('returns the string format if there is no field', () => { - const agg = aggConfig; - delete agg.params.field; - const fieldFormatter = agg.fieldFormatter(); - expect(fieldFormatter).toBeDefined(); - expect(fieldFormatter('text')).toBe('text'); - }); - - it('returns the html converter if "html" is passed in', () => { - const field = indexPattern.fields.getByName('bytes'); - expect(aggConfig.fieldFormatter('html').toString()).toBe( - field!.format.getConverterFor('html').toString() - ); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts index 659bec3f702e37..2b21c5c4868a52 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts @@ -17,8 +17,16 @@ * under the License. */ +/** + * @name AggConfig + * + * @description This class represents an aggregation, which is displayed in the left-hand nav of + * the Visualize app. + */ + import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { IAggType } from './agg_type'; import { AggGroupNames } from './agg_groups'; import { writeParams } from './agg_params'; @@ -30,20 +38,18 @@ import { FieldFormatsContentType, KBN_FIELD_TYPES, } from '../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../plugins/data/public/services'; export interface AggConfigOptions { - type: IAggType; - enabled?: boolean; + enabled: boolean; + type: string; + params: any; id?: string; - params?: Record; - schema?: string | Schema; + schema?: string; } const unknownSchema: Schema = { name: 'unknown', - title: 'Unknown', // only here for illustrative purposes + title: 'Unknown', hideCustomLabel: true, aggFilter: [], min: 1, @@ -59,6 +65,21 @@ const unknownSchema: Schema = { }, }; +const getTypeFromRegistry = (type: string): IAggType => { + // We need to inline require here, since we're having a cyclic dependency + // from somewhere inside agg_types back to AggConfig. + const aggTypes = require('../aggs').aggTypes; + const registeredType = + aggTypes.metrics.find((agg: IAggType) => agg.name === type) || + aggTypes.buckets.find((agg: IAggType) => agg.name === type); + + if (!registeredType) { + throw new Error('unknown type'); + } + + return registeredType; +}; + const getSchemaFromRegistry = (schemas: any, schema: string): Schema => { let registeredSchema = schemas ? schemas.byName[schema] : null; if (!registeredSchema) { @@ -69,13 +90,6 @@ const getSchemaFromRegistry = (schemas: any, schema: string): Schema => { return registeredSchema; }; -/** - * @name AggConfig - * - * @description This class represents an aggregation, which is displayed in the left-hand nav of - * the Visualize app. - */ - // TODO need to make a more explicit interface for this export type IAggConfig = AggConfig; @@ -87,9 +101,9 @@ export class AggConfig { * @param {array[object]} list - a list of objects, objects can be anything really * @return {array} - the list that was passed in */ - static ensureIds(list: any[]) { - const have: IAggConfig[] = []; - const haveNot: AggConfigOptions[] = []; + static ensureIds(list: AggConfig[]) { + const have: AggConfig[] = []; + const haveNot: AggConfig[] = []; list.forEach(function(obj) { (obj.id ? have : haveNot).push(obj); }); @@ -107,7 +121,7 @@ export class AggConfig { * * @return {array} list - a list of objects with id properties */ - static nextId(list: IAggConfig[]) { + static nextId(list: AggConfig[]) { return ( 1 + list.reduce(function(max, obj) { @@ -147,10 +161,10 @@ export class AggConfig { // set the params to the values from opts, or just to the defaults this.setParams(opts.params || {}); - // @ts-ignore - this.__schema = this.__schema; // @ts-ignore this.__type = this.__type; + // @ts-ignore + this.__schema = this.__schema; } /** @@ -380,8 +394,7 @@ export class AggConfig { } fieldOwnFormatter(contentType?: FieldFormatsContentType, defaultFormat?: any) { - const fieldFormatsService = getFieldFormats(); - + const fieldFormatsService = npStart.plugins.data.fieldFormats; const field = this.getField(); let format = field && field.format; if (!format) format = defaultFormat; @@ -443,8 +456,8 @@ export class AggConfig { }); } - public setType(type: IAggType) { - this.type = type; + public setType(type: string | IAggType) { + this.type = typeof type === 'string' ? getTypeFromRegistry(type) : type; } public get schema() { diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts deleted file mode 100644 index 29f16b1e4f0bf2..00000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts +++ /dev/null @@ -1,503 +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 { indexBy } from 'lodash'; -import { AggConfig } from './agg_config'; -import { AggConfigs } from './agg_configs'; -import { AggTypesRegistryStart } from './agg_types_registry'; -import { Schemas } from './schemas'; -import { AggGroupNames } from './agg_groups'; -import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; -import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; -import { - stubIndexPattern, - stubIndexPatternWithFields, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/data/public/stubs'; - -describe('AggConfigs', () => { - let indexPattern: IndexPattern; - let typesRegistry: AggTypesRegistryStart; - - beforeEach(() => { - indexPattern = stubIndexPatternWithFields as IndexPattern; - typesRegistry = mockAggTypesRegistry(); - }); - - describe('constructor', () => { - it('handles passing just a type', () => { - const configStates = [ - { - enabled: true, - type: 'histogram', - params: {}, - }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - expect(ac.aggs).toHaveLength(1); - }); - - it('attempts to ensure that all states have an id', () => { - const configStates = [ - { - enabled: true, - type: 'histogram', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - params: {}, - }, - { - enabled: true, - type: 'terms', - params: {}, - schema: 'split', - }, - ]; - - const spy = jest.spyOn(AggConfig, 'ensureIds'); - new AggConfigs(indexPattern, configStates, { typesRegistry }); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy.mock.calls[0]).toEqual([configStates]); - spy.mockRestore(); - }); - - describe('defaults', () => { - const schemas = new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: 'Simple', - min: 1, - max: 2, - defaults: [ - { schema: 'metric', type: 'count' }, - { schema: 'metric', type: 'avg' }, - { schema: 'metric', type: 'sum' }, - ], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: 'Example', - min: 0, - max: 1, - defaults: [ - { schema: 'segment', type: 'terms' }, - { schema: 'segment', type: 'filters' }, - ], - }, - ]); - - it('should only set the number of defaults defined by the max', () => { - const ac = new AggConfigs(indexPattern, [], { - schemas: schemas.all, - typesRegistry, - }); - expect(ac.bySchemaName('metric')).toHaveLength(2); - }); - - it('should set the defaults defined in the schema when none exist', () => { - const ac = new AggConfigs(indexPattern, [], { - schemas: schemas.all, - typesRegistry, - }); - expect(ac.aggs).toHaveLength(3); - }); - - it('should NOT set the defaults defined in the schema when some exist', () => { - const configStates = [ - { - enabled: true, - type: 'date_histogram', - params: {}, - schema: 'segment', - }, - ]; - const ac = new AggConfigs(indexPattern, configStates, { - schemas: schemas.all, - typesRegistry, - }); - expect(ac.aggs).toHaveLength(3); - expect(ac.bySchemaName('segment')[0].type.name).toEqual('date_histogram'); - }); - }); - }); - - describe('#createAggConfig', () => { - it('accepts a configState which is provided as an AggConfig object', () => { - const configStates = [ - { - enabled: true, - type: 'histogram', - params: {}, - }, - { - enabled: true, - type: 'date_histogram', - params: {}, - }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - expect(ac.aggs).toHaveLength(2); - - ac.createAggConfig( - new AggConfig(ac, { - enabled: true, - type: typesRegistry.get('terms'), - params: {}, - schema: 'split', - }) - ); - expect(ac.aggs).toHaveLength(3); - }); - - it('adds new AggConfig entries to AggConfigs by default', () => { - const configStates = [ - { - enabled: true, - type: 'histogram', - params: {}, - }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - expect(ac.aggs).toHaveLength(1); - - ac.createAggConfig({ - enabled: true, - type: 'terms', - params: {}, - schema: 'split', - }); - expect(ac.aggs).toHaveLength(2); - }); - - it('does not add an agg to AggConfigs if addToAggConfigs: false', () => { - const configStates = [ - { - enabled: true, - type: 'histogram', - params: {}, - }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - expect(ac.aggs).toHaveLength(1); - - ac.createAggConfig( - { - enabled: true, - type: 'terms', - params: {}, - schema: 'split', - }, - { addToAggConfigs: false } - ); - expect(ac.aggs).toHaveLength(1); - }); - }); - - describe('#getRequestAggs', () => { - it('performs a stable sort, but moves metrics to the bottom', () => { - const configStates = [ - { type: 'avg', enabled: true, params: {}, schema: 'metric' }, - { type: 'terms', enabled: true, params: {}, schema: 'split' }, - { type: 'histogram', enabled: true, params: {}, schema: 'split' }, - { type: 'sum', enabled: true, params: {}, schema: 'metric' }, - { type: 'date_histogram', enabled: true, params: {}, schema: 'segment' }, - { type: 'filters', enabled: true, params: {}, schema: 'split' }, - { type: 'percentiles', enabled: true, params: {}, schema: 'metric' }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const sorted = ac.getRequestAggs(); - const aggs = indexBy(ac.aggs, agg => agg.type.name); - - expect(sorted.shift()).toBe(aggs.terms); - expect(sorted.shift()).toBe(aggs.histogram); - expect(sorted.shift()).toBe(aggs.date_histogram); - expect(sorted.shift()).toBe(aggs.filters); - expect(sorted.shift()).toBe(aggs.avg); - expect(sorted.shift()).toBe(aggs.sum); - expect(sorted.shift()).toBe(aggs.percentiles); - expect(sorted).toHaveLength(0); - }); - }); - - describe('#getResponseAggs', () => { - it('returns all request aggs for basic aggs', () => { - const configStates = [ - { type: 'terms', enabled: true, params: {}, schema: 'split' }, - { type: 'date_histogram', enabled: true, params: {}, schema: 'segment' }, - { type: 'count', enabled: true, params: {}, schema: 'metric' }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const sorted = ac.getResponseAggs(); - const aggs = indexBy(ac.aggs, agg => agg.type.name); - - expect(sorted.shift()).toBe(aggs.terms); - expect(sorted.shift()).toBe(aggs.date_histogram); - expect(sorted.shift()).toBe(aggs.count); - expect(sorted).toHaveLength(0); - }); - - it('expands aggs that have multiple responses', () => { - const configStates = [ - { type: 'terms', enabled: true, params: {}, schema: 'split' }, - { type: 'date_histogram', enabled: true, params: {}, schema: 'segment' }, - { type: 'percentiles', enabled: true, params: { percents: [1, 2, 3] }, schema: 'metric' }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const sorted = ac.getResponseAggs(); - const aggs = indexBy(ac.aggs, agg => agg.type.name); - - expect(sorted.shift()).toBe(aggs.terms); - expect(sorted.shift()).toBe(aggs.date_histogram); - expect(sorted.shift()!.id!).toBe(aggs.percentiles.id + '.' + 1); - expect(sorted.shift()!.id!).toBe(aggs.percentiles.id + '.' + 2); - expect(sorted.shift()!.id!).toBe(aggs.percentiles.id + '.' + 3); - expect(sorted).toHaveLength(0); - }); - }); - - describe('#toDsl', () => { - const schemas = new Schemas([ - { - group: AggGroupNames.Buckets, - name: 'segment', - }, - { - group: AggGroupNames.Buckets, - name: 'split', - }, - ]); - - beforeEach(() => { - mockDataServices(); - indexPattern = stubIndexPattern as IndexPattern; - indexPattern.fields.getByName = name => (name as unknown) as IndexPatternField; - }); - - it('uses the sorted aggs', () => { - const configStates = [{ enabled: true, type: 'avg', params: { field: 'bytes' } }]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const spy = jest.spyOn(AggConfigs.prototype, 'getRequestAggs'); - ac.toDsl(); - expect(spy).toHaveBeenCalledTimes(1); - spy.mockRestore(); - }); - - it('calls aggConfig#toDsl() on each aggConfig and compiles the nested output', () => { - const configStates = [ - { enabled: true, type: 'date_histogram', params: {}, schema: 'segment' }, - { enabled: true, type: 'terms', params: {}, schema: 'split' }, - { enabled: true, type: 'count', params: {} }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { - typesRegistry, - schemas: schemas.all, - }); - - const aggInfos = ac.aggs.map(aggConfig => { - const football = {}; - aggConfig.toDsl = jest.fn().mockImplementation(() => football); - - return { - id: aggConfig.id, - football, - }; - }); - - (function recurse(lvl: Record): void { - const info = aggInfos.shift(); - if (!info) return; - - expect(lvl).toHaveProperty(info.id); - expect(lvl[info.id]).toBe(info.football); - - if (lvl[info.id].aggs) { - return recurse(lvl[info.id].aggs); - } - })(ac.toDsl()); - - expect(aggInfos).toHaveLength(1); - }); - - it("skips aggs that don't have a dsl representation", () => { - const configStates = [ - { - enabled: true, - type: 'date_histogram', - params: { field: '@timestamp', interval: '10s' }, - schema: 'segment', - }, - { - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const dsl = ac.toDsl(); - const histo = ac.byName('date_histogram')[0]; - const count = ac.byName('count')[0]; - - expect(dsl).toHaveProperty(histo.id); - expect(typeof dsl[histo.id]).toBe('object'); - expect(dsl[histo.id]).not.toHaveProperty('aggs'); - expect(dsl).not.toHaveProperty(count.id); - }); - - it('writes multiple metric aggregations at the same level', () => { - const configStates = [ - { - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { enabled: true, type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { enabled: true, type: 'min', schema: 'metric', params: { field: 'bytes' } }, - { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { - typesRegistry, - schemas: schemas.all, - }); - const dsl = ac.toDsl(); - const histo = ac.byName('date_histogram')[0]; - const metrics = ac.bySchemaGroup('metrics'); - - expect(dsl).toHaveProperty(histo.id); - expect(typeof dsl[histo.id]).toBe('object'); - expect(dsl[histo.id]).toHaveProperty('aggs'); - - metrics.forEach(metric => { - expect(dsl[histo.id].aggs).toHaveProperty(metric.id); - expect(dsl[histo.id].aggs[metric.id]).not.toHaveProperty('aggs'); - }); - }); - - it('writes multiple metric aggregations at every level if the vis is hierarchical', () => { - const configStates = [ - { enabled: true, type: 'terms', schema: 'segment', params: { field: 'bytes', orderBy: 1 } }, - { enabled: true, type: 'terms', schema: 'segment', params: { field: 'bytes', orderBy: 1 } }, - { enabled: true, id: '1', type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { enabled: true, type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { enabled: true, type: 'min', schema: 'metric', params: { field: 'bytes' } }, - { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const topLevelDsl = ac.toDsl(true); - const buckets = ac.bySchemaGroup('buckets'); - const metrics = ac.bySchemaGroup('metrics'); - - (function checkLevel(dsl) { - const bucket = buckets.shift(); - if (!bucket) return; - - expect(dsl).toHaveProperty(bucket.id); - - expect(typeof dsl[bucket.id]).toBe('object'); - expect(dsl[bucket.id]).toHaveProperty('aggs'); - - metrics.forEach((metric: AggConfig) => { - expect(dsl[bucket.id].aggs).toHaveProperty(metric.id); - expect(dsl[bucket.id].aggs[metric.id]).not.toHaveProperty('aggs'); - }); - - if (buckets.length) { - checkLevel(dsl[bucket.id].aggs); - } - })(topLevelDsl); - }); - - it('adds the parent aggs of nested metrics at every level if the vis is hierarchical', () => { - const configStates = [ - { - enabled: true, - id: '1', - type: 'avg_bucket', - schema: 'metric', - params: { - customBucket: { - id: '1-bucket', - type: 'date_histogram', - schema: 'bucketAgg', - params: { - field: '@timestamp', - interval: '10s', - }, - }, - customMetric: { - id: '1-metric', - type: 'count', - schema: 'metricAgg', - params: {}, - }, - }, - }, - { - enabled: true, - id: '2', - type: 'terms', - schema: 'bucket', - params: { - field: 'clientip', - }, - }, - { - enabled: true, - id: '3', - type: 'terms', - schema: 'bucket', - params: { - field: 'machine.os.raw', - }, - }, - ]; - - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const topLevelDsl = ac.toDsl(true)['2']; - - expect(Object.keys(topLevelDsl.aggs)).toContain('1'); - expect(Object.keys(topLevelDsl.aggs)).toContain('1-bucket'); - expect(topLevelDsl.aggs['1'].avg_bucket).toHaveProperty('buckets_path', '1-bucket>_count'); - expect(Object.keys(topLevelDsl.aggs['3'].aggs)).toContain('1'); - expect(Object.keys(topLevelDsl.aggs['3'].aggs)).toContain('1-bucket'); - expect(topLevelDsl.aggs['3'].aggs['1'].avg_bucket).toHaveProperty( - 'buckets_path', - '1-bucket>_count' - ); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index ab70e66b1e138c..8e091ed5f21ae0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -17,12 +17,17 @@ * under the License. */ -import _ from 'lodash'; -import { Assign } from '@kbn/utility-types'; +/** + * @name AggConfig + * + * @extends IndexedArray + * + * @description A "data structure"-like class with methods for indexing and + * accessing instances of AggConfig. + */ +import _ from 'lodash'; import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; -import { IAggType } from './agg_type'; -import { AggTypesRegistryStart } from './agg_types_registry'; import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { @@ -50,24 +55,6 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) { } } -export interface AggConfigsOptions { - schemas?: Schemas; - typesRegistry: AggTypesRegistryStart; -} - -export type CreateAggConfigParams = Assign; - -/** - * @name AggConfigs - * - * @description A "data structure"-like class with methods for indexing and - * accessing instances of AggConfig. This should never be instantiated directly - * outside of this plugin. Rather, downstream plugins should do this via - * `createAggConfigs()` - * - * @internal - */ - // TODO need to make a more explicit interface for this export type IAggConfigs = AggConfigs; @@ -75,31 +62,23 @@ export class AggConfigs { public indexPattern: IndexPattern; public schemas: any; public timeRange?: TimeRange; - private readonly typesRegistry: AggTypesRegistryStart; aggs: IAggConfig[]; - constructor( - indexPattern: IndexPattern, - configStates: CreateAggConfigParams[] = [], - opts: AggConfigsOptions - ) { - this.typesRegistry = opts.typesRegistry; - + constructor(indexPattern: IndexPattern, configStates = [] as any, schemas?: any) { configStates = AggConfig.ensureIds(configStates); this.aggs = []; this.indexPattern = indexPattern; - this.schemas = opts.schemas; + this.schemas = schemas; configStates.forEach((params: any) => this.createAggConfig(params)); - if (this.schemas) { - this.initializeDefaultsFromSchemas(this.schemas); + if (schemas) { + this.initializeDefaultsFromSchemas(schemas); } } - // do this wherever the schemas were passed in, & pass in state defaults instead initializeDefaultsFromSchemas(schemas: Schemas) { // Set the defaults for any schema which has them. If the defaults // for some reason has more then the max only set the max number @@ -112,11 +91,10 @@ export class AggConfigs { }) .each((schema: any) => { if (!this.aggs.find((agg: AggConfig) => agg.schema && agg.schema.name === schema.name)) { - // the result here should be passable as a configState const defaults = schema.defaults.slice(0, schema.max); _.each(defaults, defaultState => { const state = _.defaults({ id: AggConfig.nextId(this.aggs) }, defaultState); - this.createAggConfig(state as AggConfigOptions); + this.aggs.push(new AggConfig(this, state as AggConfigOptions)); }); } }) @@ -146,36 +124,28 @@ export class AggConfigs { if (!enabledOnly) return true; return agg.enabled; }; - - const aggConfigs = new AggConfigs(this.indexPattern, this.aggs.filter(filterAggs), { - schemas: this.schemas, - typesRegistry: this.typesRegistry, - }); - + const aggConfigs = new AggConfigs( + this.indexPattern, + this.aggs.filter(filterAggs), + this.schemas + ); return aggConfigs; } createAggConfig = ( - params: CreateAggConfigParams, + params: AggConfig | AggConfigOptions, { addToAggConfigs = true } = {} ) => { - const { type } = params; let aggConfig; - if (params instanceof AggConfig) { aggConfig = params; params.parent = this; } else { - aggConfig = new AggConfig(this, { - ...params, - type: typeof type === 'string' ? this.typesRegistry.get(type) : type, - }); + aggConfig = new AggConfig(this, params); } - if (addToAggConfigs) { this.aggs.push(aggConfig); } - return aggConfig as T; }; @@ -196,10 +166,10 @@ export class AggConfigs { return true; } - toDsl(hierarchical: boolean = false): Record { + toDsl(hierarchical: boolean = false) { const dslTopLvl = {}; let dslLvlCursor: Record; - let nestedMetrics: Array<{ config: AggConfig; dsl: Record }> | []; + let nestedMetrics: Array<{ config: AggConfig; dsl: any }> | []; if (hierarchical) { // collect all metrics, and filter out the ones that we won't be copying diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts index b08fcf309e9ed6..30ab272537dad1 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts @@ -23,6 +23,8 @@ import { FieldParamType } from './param_types/field'; import { OptionedParamType } from './param_types/optioned'; import { AggParamType } from '../aggs/param_types/agg'; +jest.mock('ui/new_platform'); + describe('AggParams class', () => { describe('constructor args', () => { it('accepts an array of param defs', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts index c78e56dd25887d..6d4c2d1317f505 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts @@ -19,16 +19,11 @@ import { AggType, AggTypeConfig } from './agg_type'; import { IAggConfig } from './agg_config'; -import { mockDataServices } from './test_helpers'; -import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setFieldFormats } from '../../../../../../plugins/data/public/services'; +import { npStart } from 'ui/new_platform'; -describe('AggType Class', () => { - beforeEach(() => { - mockDataServices(); - }); +jest.mock('ui/new_platform'); +describe('AggType Class', () => { describe('constructor', () => { it("requires a valid config object as it's first param", () => { expect(() => { @@ -158,10 +153,7 @@ describe('AggType Class', () => { }); it('returns default formatter', () => { - setFieldFormats({ - ...dataPluginMock.createStartContract().fieldFormats, - getDefaultInstance: jest.fn(() => 'default') as any, - }); + npStart.plugins.data.fieldFormats.getDefaultInstance = jest.fn(() => 'default') as any; const aggType = new AggType({ name: 'name', diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts index 3cd9496d3f23d0..5ccf0f65c0e921 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts @@ -19,6 +19,7 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { initParams } from './agg_params'; import { AggConfig } from './agg_config'; @@ -31,8 +32,6 @@ import { IFieldFormat, ISearchSource, } from '../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../plugins/data/public/services'; export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, @@ -66,7 +65,7 @@ export interface AggTypeConfig< const getFormat = (agg: AggConfig) => { const field = agg.getField(); - const fieldFormatsService = getFieldFormats(); + const fieldFormatsService = npStart.plugins.data.fieldFormats; return field ? field.format : fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.STRING); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts deleted file mode 100644 index 405f83e237de83..00000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts +++ /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 { - AggTypesRegistry, - AggTypesRegistrySetup, - AggTypesRegistryStart, -} from './agg_types_registry'; -import { BucketAggType } from './buckets/_bucket_agg_type'; -import { MetricAggType } from './metrics/metric_agg_type'; - -const bucketType = { name: 'terms', type: 'bucket' } as BucketAggType; -const metricType = { name: 'count', type: 'metric' } as MetricAggType; - -describe('AggTypesRegistry', () => { - let registry: AggTypesRegistry; - let setup: AggTypesRegistrySetup; - let start: AggTypesRegistryStart; - - beforeEach(() => { - registry = new AggTypesRegistry(); - setup = registry.setup(); - start = registry.start(); - }); - - it('registerBucket adds new buckets', () => { - setup.registerBucket(bucketType); - expect(start.getBuckets()).toEqual([bucketType]); - }); - - it('registerBucket throws error when registering duplicate bucket', () => { - expect(() => { - setup.registerBucket(bucketType); - setup.registerBucket(bucketType); - }).toThrow(/already been registered with name: terms/); - }); - - it('registerMetric adds new metrics', () => { - setup.registerMetric(metricType); - expect(start.getMetrics()).toEqual([metricType]); - }); - - it('registerMetric throws error when registering duplicate metric', () => { - expect(() => { - setup.registerMetric(metricType); - setup.registerMetric(metricType); - }).toThrow(/already been registered with name: count/); - }); - - it('gets either buckets or metrics by id', () => { - setup.registerBucket(bucketType); - setup.registerMetric(metricType); - expect(start.get('terms')).toEqual(bucketType); - expect(start.get('count')).toEqual(metricType); - }); - - it('getBuckets retrieves only buckets', () => { - setup.registerBucket(bucketType); - expect(start.getBuckets()).toEqual([bucketType]); - }); - - it('getMetrics retrieves only metrics', () => { - setup.registerMetric(metricType); - expect(start.getMetrics()).toEqual([metricType]); - }); - - it('getAll returns all buckets and metrics', () => { - setup.registerBucket(bucketType); - setup.registerMetric(metricType); - expect(start.getAll()).toEqual({ - buckets: [bucketType], - metrics: [metricType], - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts deleted file mode 100644 index 8a8746106ae587..00000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts +++ /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 { BucketAggType } from './buckets/_bucket_agg_type'; -import { MetricAggType } from './metrics/metric_agg_type'; - -export type AggTypesRegistrySetup = ReturnType; -export type AggTypesRegistryStart = ReturnType; - -export class AggTypesRegistry { - private readonly bucketAggs = new Map(); - private readonly metricAggs = new Map(); - - setup = () => { - return { - registerBucket: >(type: T): void => { - const { name } = type; - if (this.bucketAggs.get(name)) { - throw new Error(`Bucket agg has already been registered with name: ${name}`); - } - this.bucketAggs.set(name, type); - }, - registerMetric: >(type: T): void => { - const { name } = type; - if (this.metricAggs.get(name)) { - throw new Error(`Metric agg has already been registered with name: ${name}`); - } - this.metricAggs.set(name, type); - }, - }; - }; - - start = () => { - return { - get: (name: string) => { - return this.bucketAggs.get(name) || this.metricAggs.get(name); - }, - getBuckets: () => { - return Array.from(this.bucketAggs.values()); - }, - getMetrics: () => { - return Array.from(this.metricAggs.values()); - }, - getAll: () => { - return { - buckets: Array.from(this.bucketAggs.values()), - metrics: Array.from(this.metricAggs.values()), - }; - }, - }; - }; -} diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts index d6ab58d5250a8c..546d054c5af978 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts @@ -17,16 +17,16 @@ * under the License. */ -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; -export interface IBucketAggConfig extends IAggConfig { +export interface IBucketAggConfig extends AggConfig { type: InstanceType; } -export interface BucketAggParam +export interface BucketAggParam extends AggParamType { scriptable?: boolean; filterFieldTypes?: KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; @@ -34,12 +34,12 @@ export interface BucketAggParam const bucketType = 'buckets'; -interface BucketAggTypeConfig +interface BucketAggTypeConfig extends AggTypeConfig> { - getKey?: (bucket: any, key: any, agg: IAggConfig) => any; + getKey?: (bucket: any, key: any, agg: AggConfig) => any; } -export class BucketAggType extends AggType< +export class BucketAggType extends AggType< TBucketAggConfig, BucketAggParam > { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts index 393d3b745250f4..e196687607d198 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { i18n } from '@kbn/i18n'; import { IBucketAggConfig } from './_bucket_agg_type'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 2b47dc384bca2f..0d3f58c50a42e7 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -21,22 +21,14 @@ import moment from 'moment'; import { createFilterDateHistogram } from './date_histogram'; import { intervalOptions } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; -import { dateHistogramBucketAgg, IBucketDateHistogramAggConfig } from '../date_histogram'; +import { IBucketDateHistogramAggConfig } from '../date_histogram'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { RangeFilter } from '../../../../../../../../plugins/data/public'; -// TODO: remove this once time buckets is migrated jest.mock('ui/new_platform'); describe('AggConfig Filters', () => { describe('date_histogram', () => { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry([dateHistogramBucketAgg]); - let agg: IBucketDateHistogramAggConfig; let filter: RangeFilter; let bucketStart: any; @@ -64,7 +56,7 @@ describe('AggConfig Filters', () => { params: { field: field.name, interval, customInterval: '5d' }, }, ], - { typesRegistry } + null ); const bucketKey = 1422579600000; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts index c594c7718e58bb..41e806668337e2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts @@ -18,17 +18,16 @@ */ import moment from 'moment'; -import { dateRangeBucketAgg } from '../date_range'; import { createFilterDateRange } from './date_range'; import { fieldFormats, FieldFormatsGetConfigFn } from '../../../../../../../../plugins/data/public'; import { AggConfigs } from '../../agg_configs'; -import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; +jest.mock('ui/new_platform'); + describe('AggConfig Filters', () => { describe('Date range', () => { - const typesRegistry = mockAggTypesRegistry([dateRangeBucketAgg]); const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -56,7 +55,7 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts index 3b9c771e0f15f9..34cf996826865f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts @@ -16,21 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - -import { filtersBucketAgg } from '../filters'; import { createFilterFilters } from './filters'; import { AggConfigs } from '../../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; import { IBucketAggConfig } from '../_bucket_agg_type'; +jest.mock('ui/new_platform'); + describe('AggConfig Filters', () => { describe('filters', () => { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry([filtersBucketAgg]); - const getAggConfigs = () => { const field = { name: 'bytes', @@ -59,7 +52,7 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry } + null ); }; it('should return a filters filter', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts index b046c802c58c15..9f845847df5d9d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts @@ -16,22 +16,16 @@ * specific language governing permissions and limitations * under the License. */ - import { createFilterHistogram } from './histogram'; import { AggConfigs } from '../../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { fieldFormats, FieldFormatsGetConfigFn } from '../../../../../../../../plugins/data/public'; +jest.mock('ui/new_platform'); + describe('AggConfig Filters', () => { describe('histogram', () => { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry(); - const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -61,7 +55,7 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts index 7572c48390dc25..e92ba5cb2852a1 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts @@ -17,18 +17,17 @@ * under the License. */ -import { ipRangeBucketAgg } from '../ip_range'; import { createFilterIpRange } from './ip_range'; -import { AggConfigs, CreateAggConfigParams } from '../../agg_configs'; -import { mockAggTypesRegistry } from '../../test_helpers'; +import { AggConfigs } from '../../agg_configs'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; +jest.mock('ui/new_platform'); + describe('AggConfig Filters', () => { describe('IP range', () => { - const typesRegistry = mockAggTypesRegistry([ipRangeBucketAgg]); - const getAggConfigs = (aggs: CreateAggConfigParams[]) => { + const getAggConfigs = (aggs: Array>) => { const field = { name: 'ip', format: fieldFormats.IpFormat, @@ -43,7 +42,7 @@ describe('AggConfig Filters', () => { }, } as any; - return new AggConfigs(indexPattern, aggs, { typesRegistry }); + return new AggConfigs(indexPattern, aggs, null); }; it('should return a range filter for ip_range agg', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts index 324d4252908324..33344ca0a34845 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts @@ -17,22 +17,16 @@ * under the License. */ -import { rangeBucketAgg } from '../range'; import { createFilterRange } from './range'; import { fieldFormats, FieldFormatsGetConfigFn } from '../../../../../../../../plugins/data/public'; import { AggConfigs } from '../../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; +jest.mock('ui/new_platform'); + describe('AggConfig Filters', () => { describe('range', () => { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry([rangeBucketAgg]); - const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -62,7 +56,7 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index 6db6eb11a5f527..7c6e769437ca1d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -17,18 +17,17 @@ * under the License. */ -import { termsBucketAgg } from '../terms'; import { createFilterTerms } from './terms'; -import { AggConfigs, CreateAggConfigParams } from '../../agg_configs'; -import { mockAggTypesRegistry } from '../../test_helpers'; +import { AggConfigs } from '../../agg_configs'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { Filter, ExistsFilter } from '../../../../../../../../plugins/data/public'; +jest.mock('ui/new_platform'); + describe('AggConfig Filters', () => { describe('terms', () => { - const typesRegistry = mockAggTypesRegistry([termsBucketAgg]); - const getAggConfigs = (aggs: CreateAggConfigParams[]) => { + const getAggConfigs = (aggs: Array>) => { const indexPattern = { id: '1234', title: 'logstash-*', @@ -43,7 +42,7 @@ describe('AggConfig Filters', () => { indexPattern, }; - return new AggConfigs(indexPattern, aggs, { typesRegistry }); + return new AggConfigs(indexPattern, aggs, null); }; it('should return a match_phrase filter for terms', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts index a5368135728d49..dc0f9baa6d0cc7 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -21,7 +21,8 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -// TODO need to move TimeBuckets +import { npStart } from 'ui/new_platform'; +import { timefilter } from 'ui/timefilter'; import { TimeBuckets } from 'ui/time_buckets'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -32,8 +33,6 @@ import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getQueryService, getUiSettings } from '../../../../../../../plugins/data/public/services'; const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); @@ -41,7 +40,6 @@ const tzOffset = moment().format('Z'); const getInterval = (agg: IBucketAggConfig): string => _.get(agg, ['params', 'interval']); export const setBounds = (agg: IBucketDateHistogramAggConfig, force?: boolean) => { - const { timefilter } = getQueryService().timefilter; if (agg.buckets._alreadySet && !force) return; agg.buckets._alreadySet = true; const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; @@ -223,7 +221,7 @@ export const dateHistogramBucketAgg = new BucketAggType { - beforeEach(() => { - mockDataServices(); - }); +import { npStart } from 'ui/new_platform'; - const typesRegistry = mockAggTypesRegistry([dateRangeBucketAgg]); +jest.mock('ui/new_platform'); +describe('date_range params', () => { const getAggConfigs = (params: Record = {}, hasIncludeTypeMeta: boolean = true) => { const field = { name: 'bytes', @@ -67,7 +58,7 @@ describe('date_range params', () => { params, }, ], - { typesRegistry } + null ); }; @@ -104,11 +95,7 @@ describe('date_range params', () => { }); it('should use the Kibana time_zone if no parameter specified', () => { - const core = coreMock.createStart(); - setUiSettings({ - ...core.uiSettings, - get: () => 'kibanaTimeZone' as any, - }); + npStart.core.uiSettings.get = jest.fn(() => 'kibanaTimeZone' as any); const aggConfigs = getAggConfigs( { @@ -119,8 +106,6 @@ describe('date_range params', () => { const dateRange = aggConfigs.aggs[0]; const params = dateRange.toDsl()[BUCKET_TYPES.DATE_RANGE]; - setUiSettings(core.uiSettings); // clean up - expect(params.time_zone).toBe('kibanaTimeZone'); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts index 933cdd0577f8da..1dc24ca80035c0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts @@ -16,20 +16,18 @@ * specific language governing permissions and limitations * under the License. */ - import { get } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import { convertDateRangeToString, DateRangeKey } from './lib/date_range'; import { BUCKET_TYPES } from './bucket_agg_types'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterDateRange } from './create_filter/date_range'; import { KBN_FIELD_TYPES, fieldFormats } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats, getUiSettings } from '../../../../../../../plugins/data/public/services'; -import { convertDateRangeToString, DateRangeKey } from './lib/date_range'; -export { convertDateRangeToString, DateRangeKey }; // for BWC +export { convertDateRangeToString, DateRangeKey }; const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', { defaultMessage: 'Date Range', @@ -43,7 +41,7 @@ export const dateRangeBucketAgg = new BucketAggType({ return { from, to }; }, getFormat(agg) { - const fieldFormatsService = getFieldFormats(); + const fieldFormatsService = npStart.plugins.data.fieldFormats; const formatter = agg.fieldOwnFormatter( fieldFormats.TEXT_CONTEXT_TYPE, @@ -94,7 +92,7 @@ export const dateRangeBucketAgg = new BucketAggType({ ]); } if (!tz) { - const config = getUiSettings(); + const config = npStart.core.uiSettings; const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); const isDefaultTimezone = config.isDefault('dateFormat:tz'); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts index 80efc0cf92071d..b52e2d6cfd4df4 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { i18n } from '@kbn/i18n'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts index 2852f3e4bdf464..6eaf788b83c04a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts @@ -18,21 +18,19 @@ */ import _ from 'lodash'; +import angular from 'angular'; + import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; - import { createFilterFilters } from './create_filter/filters'; -import { toAngularJSON } from '../utils'; import { BucketAggType } from './_bucket_agg_type'; -import { BUCKET_TYPES } from './bucket_agg_types'; import { Storage } from '../../../../../../../plugins/kibana_utils/public'; - import { getQueryLog, esQuery, Query } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../../../plugins/data/public/services'; +import { BUCKET_TYPES } from './bucket_agg_types'; const config = chrome.getUiSettingsClient(); +const storage = new Storage(window.localStorage); const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', { defaultMessage: 'Filters', @@ -54,17 +52,15 @@ export const filtersBucketAgg = new BucketAggType({ params: [ { name: 'filters', - // TODO need to get rid of reference to `config` below default: [{ input: { query: '', language: config.get('search:queryLanguage') }, label: '' }], write(aggConfig, output) { - const uiSettings = getUiSettings(); const inFilters: FilterValue[] = aggConfig.params.filters; if (!_.size(inFilters)) return; inFilters.forEach(filter => { const persistedLog = getQueryLog( - uiSettings, - new Storage(window.localStorage), + config, + storage, 'vis_default_editor', filter.input.language ); @@ -81,13 +77,7 @@ export const filtersBucketAgg = new BucketAggType({ return; } - const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - const query = esQuery.buildEsQuery( - aggConfig.getIndexPattern(), - [input], - [], - esQueryConfigs - ); + const query = esQuery.buildEsQuery(aggConfig.getIndexPattern(), [input], [], config); if (!query) { console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console @@ -100,7 +90,7 @@ export const filtersBucketAgg = new BucketAggType({ matchAllLabel || (typeof filter.input.query === 'string' ? filter.input.query - : toAngularJSON(filter.input.query)); + : angular.toJson(filter.input.query)); filters[label] = { query }; }, {} diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts index 09dd03c759155e..f0ad5954764867 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -19,13 +19,12 @@ import { geoHashBucketAgg } from './geo_hash'; import { AggConfigs, IAggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketAggConfig } from './_bucket_agg_type'; +jest.mock('ui/new_platform'); + describe('Geohash Agg', () => { - // const typesRegistry = mockAggTypesRegistry([geoHashBucketAgg]); - const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (params?: Record) => { const indexPattern = { id: '1234', @@ -63,7 +62,7 @@ describe('Geohash Agg', () => { }, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts index 9142a30338163c..57e8f6e8c5ded4 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; +import { AggConfigOptions } from '../agg_config'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -56,7 +57,7 @@ export const geoTileBucketAgg = new BucketAggType({ aggs.push(agg); if (useGeocentroid) { - const aggConfig = { + const aggConfig: AggConfigOptions = { type: METRIC_TYPES.GEO_CENTROID, enabled: true, params: { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts index 11dc8e42fd6538..4e89d7db1ff647 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts @@ -17,23 +17,16 @@ * under the License. */ -import { AggConfigs } from '../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; +import { npStart } from 'ui/new_platform'; +import { AggConfigs } from '../index'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, histogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './_bucket_agg_type'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setUiSettings } from '../../../../../../../plugins/data/public/services'; -describe('Histogram Agg', () => { - beforeEach(() => { - mockDataServices(); - }); +jest.mock('ui/new_platform'); - const typesRegistry = mockAggTypesRegistry([histogramBucketAgg]); - - const getAggConfigs = (params: Record) => { +describe('Histogram Agg', () => { + const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', title: 'logstash-*', @@ -52,13 +45,16 @@ describe('Histogram Agg', () => { indexPattern, [ { + field: { + name: 'field', + }, id: 'test', type: BUCKET_TYPES.HISTOGRAM, schema: 'segment', params, }, ], - { typesRegistry } + null ); }; @@ -162,15 +158,10 @@ describe('Histogram Agg', () => { aggConfig.setAutoBounds(autoBounds); } - const core = coreMock.createStart(); - setUiSettings({ - ...core.uiSettings, - get: () => maxBars as any, - }); + // mock histogram:maxBars value; + npStart.core.uiSettings.get = jest.fn(() => maxBars as any); - const interval = aggConfig.write(aggConfigs).params; - setUiSettings(core.uiSettings); // clean up - return interval; + return aggConfig.write(aggConfigs).params; }; it('will respect the histogram:maxBars setting', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts index 70df2f230db094..f7e9ef45961e04 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts @@ -19,13 +19,13 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { toastNotifications } from 'ui/notify'; +import { npStart } from 'ui/new_platform'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; -import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getNotifications, getUiSettings } from '../../../../../../../plugins/data/public/services'; +import { BUCKET_TYPES } from './bucket_agg_types'; export interface AutoBounds { min: number; @@ -37,6 +37,8 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { getAutoBounds: () => AutoBounds; } +const getUIConfig = () => npStart.core.uiSettings; + export const histogramBucketAgg = new BucketAggType({ name: BUCKET_TYPES.HISTOGRAM, title: i18n.translate('data.search.aggs.buckets.histogramTitle', { @@ -114,7 +116,7 @@ export const histogramBucketAgg = new BucketAggType({ }) .catch((e: Error) => { if (e.name === 'AbortError') return; - getNotifications().toasts.addWarning( + toastNotifications.addWarning( i18n.translate('data.search.aggs.histogram.missingMaxMinValuesWarning', { defaultMessage: 'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.', @@ -134,7 +136,7 @@ export const histogramBucketAgg = new BucketAggType({ const range = autoBounds.max - autoBounds.min; const bars = range / interval; - const config = getUiSettings(); + const config = getUIConfig(); if (bars > config.get('histogram:maxBars')) { const minInterval = range / config.get('histogram:maxBars'); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts index 3fb464d8fa7a8b..91bdf53e7f8091 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts @@ -19,17 +19,15 @@ import { noop, map, omit, isNull } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import { IpRangeKey, convertIPRangeToString } from './lib/ip_range'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; +// @ts-ignore import { createFilterIpRange } from './create_filter/ip_range'; import { KBN_FIELD_TYPES, fieldFormats } from '../../../../../../../plugins/data/public'; - -import { IpRangeKey, convertIPRangeToString } from './lib/ip_range'; -export { IpRangeKey, convertIPRangeToString }; // for BWC - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; +export { IpRangeKey, convertIPRangeToString }; const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', { defaultMessage: 'IPv4 Range', @@ -46,7 +44,7 @@ export const ipRangeBucketAgg = new BucketAggType({ return { type: 'range', from: bucket.from, to: bucket.to }; }, getFormat(agg) { - const fieldFormatsService = getFieldFormats(); + const fieldFormatsService = npStart.plugins.data.fieldFormats; const formatter = agg.fieldOwnFormatter( fieldFormats.TEXT_CONTEXT_TYPE, fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.IP) diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts index d94477b588f8d1..77e84e044de55a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts @@ -19,10 +19,10 @@ import { isString, isObject } from 'lodash'; import { IBucketAggConfig, BucketAggType, BucketAggParam } from './_bucket_agg_type'; -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; export const isType = (type: string) => { - return (agg: IAggConfig): boolean => { + return (agg: AggConfig): boolean => { const field = agg.params.field; return field && field.type === type; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts index 096b19fe7de66d..b1b0c4bc30a58e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts @@ -17,12 +17,12 @@ * under the License. */ -import { rangeBucketAgg } from './range'; import { AggConfigs } from '../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { FieldFormatsGetConfigFn, fieldFormats } from '../../../../../../../plugins/data/public'; +jest.mock('ui/new_platform'); + const buckets = [ { to: 1024, @@ -44,12 +44,6 @@ const buckets = [ ]; describe('Range Agg', () => { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry([rangeBucketAgg]); - const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -86,7 +80,7 @@ describe('Range Agg', () => { }, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts index cee3ed506c29c4..37b829bfc20fb2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts @@ -17,16 +17,17 @@ * under the License. */ -import { AggConfigs, IAggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; +import { AggConfigs } from '../index'; +import { IAggConfigs } from '../types'; import { BUCKET_TYPES } from './bucket_agg_types'; import { significantTermsBucketAgg } from './significant_terms'; import { IBucketAggConfig } from './_bucket_agg_type'; +jest.mock('ui/new_platform'); + describe('Significant Terms Agg', () => { describe('order agg editor UI', () => { describe('convert include/exclude from old format', () => { - const typesRegistry = mockAggTypesRegistry([significantTermsBucketAgg]); const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', @@ -52,7 +53,7 @@ describe('Significant Terms Agg', () => { params, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts index 9a4f28afd3edf2..24ac332ae4d55c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts @@ -17,13 +17,13 @@ * under the License. */ -import { AggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; +import { AggConfigs } from '../index'; import { BUCKET_TYPES } from './bucket_agg_types'; +jest.mock('ui/new_platform'); + describe('Terms Agg', () => { describe('order agg editor UI', () => { - const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', @@ -48,7 +48,7 @@ describe('Terms Agg', () => { type: BUCKET_TYPES.TERMS, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts index 0de1c31d02f961..cc1288d339692f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts @@ -19,12 +19,13 @@ import { IndexPattern } from '../../../../../../../plugins/data/public'; import { AggTypeFilters } from './agg_type_filters'; -import { IAggConfig, IAggType } from '../types'; +import { AggConfig } from '..'; +import { IAggType } from '../types'; describe('AggTypeFilters', () => { let registry: AggTypeFilters; const indexPattern = ({ id: '1234', fields: [], title: 'foo' } as unknown) as IndexPattern; - const aggConfig = {} as IAggConfig; + const aggConfig = {} as AggConfig; beforeEach(() => { registry = new AggTypeFilters(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts index 13a4cc0856b090..d3b38ce041d7ee 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { IndexPattern } from 'src/plugins/data/public'; import { IAggConfig, IAggType } from '../types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts index 32cda7b950e93d..431e1161e0dbdf 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import expect from '@kbn/expect'; import { propFilter } from './prop_filter'; describe('prop filter', () => { @@ -46,48 +47,48 @@ describe('prop filter', () => { it('returns list when no filters are provided', () => { const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects)).toEqual(objects); + expect(nameFilter(objects)).to.eql(objects); }); it('returns list when empty list of filters is provided', () => { const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects, [])).toEqual(objects); + expect(nameFilter(objects, [])).to.eql(objects); }); it('should keep only the tables', () => { const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects, 'table')).toEqual(getObjects('table', 'table')); + expect(nameFilter(objects, 'table')).to.eql(getObjects('table', 'table')); }); it('should support comma-separated values', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, 'table,line')).toEqual(getObjects('table', 'line')); + expect(nameFilter(objects, 'table,line')).to.eql(getObjects('table', 'line')); }); it('should support an array of values', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, ['table', 'line'])).toEqual(getObjects('table', 'line')); + expect(nameFilter(objects, ['table', 'line'])).to.eql(getObjects('table', 'line')); }); it('should return all objects', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, '*')).toEqual(objects); + expect(nameFilter(objects, '*')).to.eql(objects); }); it('should allow negation', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, ['!line'])).toEqual(getObjects('table', 'pie')); + expect(nameFilter(objects, ['!line'])).to.eql(getObjects('table', 'pie')); }); it('should support a function for specifying what should be kept', () => { const objects = getObjects('table', 'line', 'pie'); const line = (value: string) => value === 'line'; - expect(nameFilter(objects, line)).toEqual(getObjects('line')); + expect(nameFilter(objects, line)).to.eql(getObjects('line')); }); it('gracefully handles a filter function with zero arity', () => { const objects = getObjects('table', 'line', 'pie'); const rejectEverything = () => false; - expect(nameFilter(objects, rejectEverything)).toEqual([]); + expect(nameFilter(objects, rejectEverything)).to.eql([]); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.test.ts b/src/legacy/core_plugins/data/public/search/aggs/index.test.ts index 4d0cd55b09d533..a867769a77fc1d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.test.ts @@ -25,6 +25,8 @@ import { isMetricAggType } from './metrics/metric_agg_type'; const bucketAggs = aggTypes.buckets; const metricAggs = aggTypes.metrics; +jest.mock('ui/new_platform'); + describe('AggTypesComponent', () => { describe('bucket aggs', () => { it('all extend BucketAggType', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index f6914c36f6c05c..0bdb92b8de65e8 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -17,13 +17,8 @@ * under the License. */ -export { - AggTypesRegistry, - AggTypesRegistrySetup, - AggTypesRegistryStart, -} from './agg_types_registry'; -export { AggType } from './agg_type'; export { aggTypes } from './agg_types'; +export { AggType } from './agg_type'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; export { FieldParamType } from './param_types'; @@ -57,4 +52,4 @@ export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; // types -export { CreateAggConfigParams, IAggConfig, IAggConfigs } from './types'; +export { IAggConfig, IAggConfigs } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts index 11bb5592747297..9fb28f8631bc63 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; + import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts index 0668a9bcf57a8f..83837f0de51146 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; + import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts index 8f728cb5e7e426..d96197693dc2ee 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts index 4f7b6e555ca33e..147e9255210887 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts @@ -18,11 +18,10 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; const uniqueCountTitle = i18n.translate('data.search.aggs.metrics.uniqueCountTitle', { defaultMessage: 'Unique Count', @@ -38,7 +37,7 @@ export const cardinalityMetricAgg = new MetricAggType({ }); }, getFormat() { - const fieldFormatsService = getFieldFormats(); + const fieldFormatsService = npStart.plugins.data.fieldFormats; return fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts index 8b3e0a488c68a1..14a9bd073ff2bc 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts @@ -18,11 +18,10 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; -import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; export const countMetricAgg = new MetricAggType({ name: METRIC_TYPES.COUNT, @@ -36,7 +35,7 @@ export const countMetricAgg = new MetricAggType({ }); }, getFormat() { - const fieldFormatsService = getFieldFormats(); + const fieldFormatsService = npStart.plugins.data.fieldFormats; return fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts index 00d866e6f2b3ed..054543de3dd067 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { assign } from 'lodash'; import { IMetricAggConfig } from '../metric_agg_type'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 88549ee3019ee6..e24aca08271c72 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -23,6 +23,7 @@ import { noop, identity } from 'lodash'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; + import { Schemas } from '../../schemas'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index 05e009cc9da30e..e7c98e575fdb4f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -21,6 +21,7 @@ import { identity } from 'lodash'; import { i18n } from '@kbn/i18n'; import { siblingPipelineAggWriter } from './sibling_pipeline_agg_writer'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; + import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { Schemas } from '../../schemas'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts index ad55837ec9a300..4755a873e69779 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts @@ -17,16 +17,15 @@ * under the License. */ -import { medianMetricAgg } from './median'; import { AggConfigs, IAggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +jest.mock('ui/new_platform'); + describe('AggTypeMetricMedianProvider class', () => { let aggConfigs: IAggConfigs; beforeEach(() => { - const typesRegistry = mockAggTypesRegistry([medianMetricAgg]); const field = { name: 'bytes', }; @@ -51,7 +50,7 @@ describe('AggTypeMetricMedianProvider class', () => { }, }, ], - { typesRegistry } + null ); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts index 68fc98261118c2..53a5ffff418f10 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ - import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; + +// @ts-ignore +import { percentilesMetricAgg } from './percentiles'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; const medianTitle = i18n.translate('data.search.aggs.metrics.medianTitle', { diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts index 952dcc96de8330..3bae7b92618dcd 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts @@ -18,14 +18,13 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; -import { FilterFieldTypes } from '../param_types/field'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; +import { FilterFieldTypes } from '../param_types/field'; export interface IMetricAggConfig extends AggConfig { type: InstanceType; @@ -79,7 +78,7 @@ export class MetricAggType { - const fieldFormatsService = getFieldFormats(); + const fieldFormatsService = npStart.plugins.data.fieldFormats; const field = agg.getField(); return field ? field.format diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts index 1806c6d9d77107..48851051634356 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index 58b4ee530a8c2d..11fc39c20bdc42 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -17,12 +17,12 @@ * under the License. */ +import sinon from 'sinon'; import { derivativeMetricAgg } from './derivative'; import { cumulativeSumMetricAgg } from './cumulative_sum'; import { movingAvgMetricAgg } from './moving_avg'; import { serialDiffMetricAgg } from './serial_diff'; import { AggConfigs } from '../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; jest.mock('../schemas', () => { @@ -34,13 +34,9 @@ jest.mock('../schemas', () => { }; }); -describe('parent pipeline aggs', function() { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry(); +jest.mock('ui/new_platform'); +describe('parent pipeline aggs', function() { const metrics = [ { name: 'derivative', title: 'Derivative', provider: derivativeMetricAgg }, { name: 'cumulative_sum', title: 'Cumulative Sum', provider: cumulativeSumMetricAgg }, @@ -98,7 +94,7 @@ describe('parent pipeline aggs', function() { schema: 'metric', }, ], - { typesRegistry } + null ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) @@ -224,16 +220,16 @@ describe('parent pipeline aggs', function() { }); const searchSource: any = {}; - const customMetricSpy = jest.fn(); + const customMetricSpy = sinon.spy(); const customMetric = aggConfig.params.customMetric; // Attach a modifyAggConfigOnSearchRequestStart with a spy to the first parameter customMetric.type.params[0].modifyAggConfigOnSearchRequestStart = customMetricSpy; aggConfig.type.params.forEach(param => { - param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource, {}); + param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource); }); - expect(customMetricSpy.mock.calls[0]).toEqual([customMetric, searchSource, {}]); + expect(customMetricSpy.calledWith(customMetric, searchSource)).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index 628f1cd204ee5e..655e918ce07deb 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -19,16 +19,14 @@ import { IPercentileRanksAggConfig, percentileRanksMetricAgg } from './percentile_ranks'; import { AggConfigs, IAggConfigs } from '../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +jest.mock('ui/new_platform'); + describe('AggTypesMetricsPercentileRanksProvider class', function() { let aggConfigs: IAggConfigs; beforeEach(() => { - mockDataServices(); - - const typesRegistry = mockAggTypesRegistry([percentileRanksMetricAgg]); const field = { name: 'bytes', }; @@ -60,7 +58,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { }, }, ], - { typesRegistry } + null ); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts index 1d640a9c1fa42f..38b47a7e97d2f2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts @@ -18,17 +18,20 @@ */ import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; + import { getPercentileValue } from './percentiles_get_value'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormats, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; // required by the values editor + export type IPercentileRanksAggConfig = IResponseAggConfig; +const getFieldFormats = () => npStart.plugins.data.fieldFormats; + const valueProps = { makeLabel(this: IPercentileRanksAggConfig) { const fieldFormatsService = getFieldFormats(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts index e077bc0f8c7737..dd1aaca973e473 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -19,14 +19,14 @@ import { IPercentileAggConfig, percentilesMetricAgg } from './percentiles'; import { AggConfigs, IAggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +jest.mock('ui/new_platform'); + describe('AggTypesMetricsPercentilesProvider class', () => { let aggConfigs: IAggConfigs; beforeEach(() => { - const typesRegistry = mockAggTypesRegistry([percentilesMetricAgg]); const field = { name: 'bytes', }; @@ -58,7 +58,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { }, }, ], - { typesRegistry } + null ); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts index 49e927d07d8dd0..39dc0d0f181e92 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts @@ -18,11 +18,15 @@ */ import { i18n } from '@kbn/i18n'; + import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; + import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; + +// @ts-ignore import { ordinalSuffix } from './lib/ordinal_suffix'; export type IPercentileAggConfig = IResponseAggConfig; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index d3456bacceb6ab..d643cf0d2a4781 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { spy } from 'sinon'; import { bucketSumMetricAgg } from './bucket_sum'; import { bucketAvgMetricAgg } from './bucket_avg'; import { bucketMinMetricAgg } from './bucket_min'; @@ -24,7 +25,6 @@ import { bucketMaxMetricAgg } from './bucket_max'; import { AggConfigs } from '../agg_configs'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; -import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; jest.mock('../schemas', () => { class MockedSchemas { @@ -35,13 +35,9 @@ jest.mock('../schemas', () => { }; }); -describe('sibling pipeline aggs', () => { - beforeEach(() => { - mockDataServices(); - }); - - const typesRegistry = mockAggTypesRegistry(); +jest.mock('ui/new_platform'); +describe('sibling pipeline aggs', () => { const metrics = [ { name: 'sum_bucket', title: 'Overall Sum', provider: bucketSumMetricAgg }, { name: 'avg_bucket', title: 'Overall Average', provider: bucketAvgMetricAgg }, @@ -100,7 +96,7 @@ describe('sibling pipeline aggs', () => { }, }, ], - { typesRegistry } + null ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) @@ -166,8 +162,8 @@ describe('sibling pipeline aggs', () => { init(); const searchSource: any = {}; - const customMetricSpy = jest.fn(); - const customBucketSpy = jest.fn(); + const customMetricSpy = spy(); + const customBucketSpy = spy(); const { customMetric, customBucket } = aggConfig.params; // Attach a modifyAggConfigOnSearchRequestStart with a spy to the first parameter @@ -175,11 +171,11 @@ describe('sibling pipeline aggs', () => { customBucket.type.params[0].modifyAggConfigOnSearchRequestStart = customBucketSpy; aggConfig.type.params.forEach(param => { - param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource, {}); + param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource); }); - expect(customMetricSpy.mock.calls[0]).toEqual([customMetric, searchSource, {}]); - expect(customBucketSpy.mock.calls[0]).toEqual([customBucket, searchSource, {}]); + expect(customMetricSpy.calledWith(customMetric, searchSource)).toBe(true); + expect(customBucketSpy.calledWith(customBucket, searchSource)).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts index 0679831b1e6ac8..3125026a521854 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts @@ -19,11 +19,11 @@ import { IStdDevAggConfig, stdDeviationMetricAgg } from './std_deviation'; import { AggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +jest.mock('ui/new_platform'); + describe('AggTypeMetricStandardDeviationProvider class', () => { - const typesRegistry = mockAggTypesRegistry([stdDeviationMetricAgg]); const getAggConfigs = (customLabel?: string) => { const field = { name: 'memory', @@ -52,7 +52,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { }, }, ], - { typesRegistry } + null ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts index ad1f42f5c563e6..a973de4fe8659e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts @@ -20,10 +20,11 @@ import { dropRight, last } from 'lodash'; import { topHitMetricAgg } from './top_hit'; import { AggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig } from './metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +jest.mock('ui/new_platform'); + describe('Top hit metric', () => { let aggDsl: Record; let aggConfig: IMetricAggConfig; @@ -36,7 +37,6 @@ describe('Top hit metric', () => { fieldType = KBN_FIELD_TYPES.NUMBER, size = 1, }: any) => { - const typesRegistry = mockAggTypesRegistry([topHitMetricAgg]); const field = { name: fieldName, displayName: fieldName, @@ -81,7 +81,7 @@ describe('Top hit metric', () => { params, }, ], - { typesRegistry } + null ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts index d31abe64491d0d..2e7c11004b472e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts @@ -17,10 +17,10 @@ * under the License. */ -import { AggConfig, IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; import { BaseParamType } from './base'; -export class AggParamType extends BaseParamType< +export class AggParamType extends BaseParamType< TAggConfig > { makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts index 95ad71a616ab27..1523cb03eb966c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts @@ -18,10 +18,10 @@ */ import { IAggConfigs } from '../agg_configs'; -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; import { FetchOptions, ISearchSource } from '../../../../../../../plugins/data/public'; -export class BaseParamType { +export class BaseParamType { name: string; type: string; displayName: string; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts index 7338c41f920d72..fa88754ac60b94 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts @@ -25,6 +25,8 @@ import { IAggConfig } from '../agg_config'; import { IMetricAggConfig } from '../metrics/metric_agg_type'; import { Schema } from '../schemas'; +jest.mock('ui/new_platform'); + describe('Field', () => { const indexPattern = { id: '1234', diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts index bb5707cbb482ec..40c30f6210a833 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { isFunction } from 'lodash'; +import { npStart } from 'ui/new_platform'; import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; @@ -29,8 +30,6 @@ import { indexPatterns, KBN_FIELD_TYPES, } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getNotifications } from '../../../../../../../plugins/data/public/services'; const filterByType = propFilter('type'); @@ -94,7 +93,7 @@ export class FieldParamType extends BaseParamType { // @ts-ignore const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName); if (!validField) { - getNotifications().toasts.addDanger( + npStart.core.notifications.toasts.addDanger( i18n.translate( 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts index 1a453a225797db..bc36bb46d3d166 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts @@ -17,26 +17,27 @@ * under the License. */ +import { IndexedArray } from 'ui/indexed_array'; import { AggTypeFieldFilters } from './field_filters'; -import { IAggConfig } from '../../agg_config'; +import { AggConfig } from '../../agg_config'; import { IndexPatternField } from '../../../../../../../../plugins/data/public'; describe('AggTypeFieldFilters', () => { let registry: AggTypeFieldFilters; - const aggConfig = {} as IAggConfig; + const aggConfig = {} as AggConfig; beforeEach(() => { registry = new AggTypeFieldFilters(); }); it('should filter nothing without registered filters', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; const filtered = registry.filter(fields, aggConfig); expect(filtered).toEqual(fields); }); it('should pass all fields to the registered filter', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; const filter = jest.fn(); registry.addFilter(filter); registry.filter(fields, aggConfig); @@ -45,7 +46,7 @@ describe('AggTypeFieldFilters', () => { }); it('should allow registered filters to filter out fields', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; let filtered = registry.filter(fields, aggConfig); expect(filtered).toEqual(fields); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts index 1cbf0c9ae36245..7d1348ab5423be 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts @@ -17,9 +17,9 @@ * under the License. */ import { IndexPatternField } from 'src/plugins/data/public'; -import { IAggConfig } from '../../agg_config'; +import { AggConfig } from '../../agg_config'; -type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: IAggConfig) => boolean; +type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: AggConfig) => boolean; /** * A registry to store {@link AggTypeFieldFilter} which are used to filter down @@ -41,11 +41,11 @@ class AggTypeFieldFilters { /** * Returns the {@link any|fields} filtered by all registered filters. * - * @param fields An array of fields that will be filtered down by this registry. + * @param fields An IndexedArray of fields that will be filtered down by this registry. * @param aggConfig The aggConfig for which the returning list will be used. * @return A filtered list of the passed fields. */ - public filter(fields: IndexPatternField[], aggConfig: IAggConfig) { + public filter(fields: IndexPatternField[], aggConfig: AggConfig) { const allFilters = Array.from(this.filters); const allowedAggTypeFields = fields.filter(field => { const isAggTypeFieldAllowed = allFilters.every(filter => filter(field, aggConfig)); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts index 12fd29b3a14527..827299814c62aa 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts @@ -19,11 +19,13 @@ import { BaseParamType } from './base'; import { JsonParamType } from './json'; -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; + +jest.mock('ui/new_platform'); describe('JSON', function() { const paramName = 'json_test'; - let aggConfig: IAggConfig; + let aggConfig: AggConfig; let output: Record; const initAggParam = (config: Record = {}) => @@ -34,7 +36,7 @@ describe('JSON', function() { }); beforeEach(function() { - aggConfig = { params: {} } as IAggConfig; + aggConfig = { params: {} } as AggConfig; output = { params: {} }; }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts index bf85b3b890c358..771919b0bb56b9 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; import { BaseParamType } from './base'; export class JsonParamType extends BaseParamType { @@ -29,7 +29,7 @@ export class JsonParamType extends BaseParamType { this.name = config.name || 'json'; if (!config.write) { - this.write = (aggConfig: IAggConfig, output: Record) => { + this.write = (aggConfig: AggConfig, output: Record) => { let paramJson; const param = aggConfig.params[this.name]; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts index c03d6cdfa1c700..6b58d81914097c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts @@ -20,6 +20,8 @@ import { BaseParamType } from './base'; import { OptionedParamType } from './optioned'; +jest.mock('ui/new_platform'); + describe('Optioned', () => { describe('constructor', () => { it('it is an instance of BaseParamType', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts index 9eb7ceda607117..5ffda3740af49f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts @@ -17,14 +17,14 @@ * under the License. */ -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; import { BaseParamType } from './base'; export interface OptionedValueProp { value: string; text: string; disabled?: boolean; - isCompatible: (agg: IAggConfig) => boolean; + isCompatible: (agg: AggConfig) => boolean; } export interface OptionedParamEditorProps { @@ -40,7 +40,7 @@ export class OptionedParamType extends BaseParamType { super(config); if (!config.write) { - this.write = (aggConfig: IAggConfig, output: Record) => { + this.write = (aggConfig: AggConfig, output: Record) => { output.params[this.name] = aggConfig.params[this.name].value; }; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts index 29ec9741611a37..fd5ccebde993eb 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts @@ -19,11 +19,13 @@ import { BaseParamType } from './base'; import { StringParamType } from './string'; -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; + +jest.mock('ui/new_platform'); describe('String', function() { let paramName = 'json_test'; - let aggConfig: IAggConfig; + let aggConfig: AggConfig; let output: Record; const initAggParam = (config: Record = {}) => @@ -34,7 +36,7 @@ describe('String', function() { }); beforeEach(() => { - aggConfig = { params: {} } as IAggConfig; + aggConfig = { params: {} } as AggConfig; output = { params: {} }; }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts index 750606eb8433bf..58ba99f8a6d636 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IAggConfig } from '../agg_config'; +import { AggConfig } from '../agg_config'; import { BaseParamType } from './base'; export class StringParamType extends BaseParamType { @@ -25,7 +25,7 @@ export class StringParamType extends BaseParamType { super(config); if (!config.write) { - this.write = (aggConfig: IAggConfig, output: Record) => { + this.write = (aggConfig: AggConfig, output: Record) => { if (aggConfig.params[this.name] && aggConfig.params[this.name].length) { output.params[this.name] = aggConfig.params[this.name]; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts deleted file mode 100644 index d6bb7938664932..00000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts +++ /dev/null @@ -1,57 +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 { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry'; -import { aggTypes } from '../agg_types'; -import { BucketAggType } from '../buckets/_bucket_agg_type'; -import { MetricAggType } from '../metrics/metric_agg_type'; - -/** - * Testing utility which creates a new instance of AggTypesRegistry, - * registers the provided agg types, and returns AggTypesRegistry.start() - * - * This is useful if your test depends on a certain agg type to be present - * in the registry. - * - * @param [types] - Optional array of AggTypes to register. - * If no value is provided, all default types will be registered. - * - * @internal - */ -export function mockAggTypesRegistry | MetricAggType>( - types?: T[] -): AggTypesRegistryStart { - const registry = new AggTypesRegistry(); - const registrySetup = registry.setup(); - - if (types) { - types.forEach(type => { - if (type instanceof BucketAggType) { - registrySetup.registerBucket(type); - } else if (type instanceof MetricAggType) { - registrySetup.registerMetric(type); - } - }); - } else { - aggTypes.buckets.forEach(type => registrySetup.registerBucket(type)); - aggTypes.metrics.forEach(type => registrySetup.registerMetric(type)); - } - - return registry.start(); -} diff --git a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts deleted file mode 100644 index c4e78ab8f64226..00000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts +++ /dev/null @@ -1,54 +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 { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { dataPluginMock } from '../../../../../../../plugins/data/public/mocks'; -import { searchStartMock } from '../../mocks'; -import { setSearchServiceShim } from '../../../services'; -import { - setFieldFormats, - setIndexPatterns, - setNotifications, - setOverlays, - setQueryService, - setSearchService, - setUiSettings, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/data/public/services'; - -/** - * Testing helper which calls all of the service setters used in the - * data plugin. Services are added using their provided mocks. - * - * @internal - */ -export function mockDataServices() { - const core = coreMock.createStart(); - const data = dataPluginMock.createStartContract(); - const searchShim = searchStartMock(); - - setSearchServiceShim(searchShim); - setFieldFormats(data.fieldFormats); - setIndexPatterns(data.indexPatterns); - setNotifications(core.notifications); - setOverlays(core.overlays); - setQueryService(data.query); - setSearchService(data.search); - setUiSettings(core.uiSettings); -} diff --git a/src/legacy/core_plugins/data/public/search/aggs/types.ts b/src/legacy/core_plugins/data/public/search/aggs/types.ts index 5d02f426b58967..2c918abf99fcaf 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/types.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/types.ts @@ -18,7 +18,7 @@ */ export { IAggConfig } from './agg_config'; -export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; +export { IAggConfigs } from './agg_configs'; export { IAggType } from './agg_type'; export { AggParam, AggParamOption } from './agg_params'; export { IFieldParamType } from './param_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx b/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx index c0662c98755a3d..a3c7f24f3927d8 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx +++ b/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx @@ -19,6 +19,8 @@ import { isValidJson } from './utils'; +jest.mock('ui/new_platform'); + const input = { valid: '{ "test": "json input" }', invalid: 'strings are not json', diff --git a/src/legacy/core_plugins/data/public/search/aggs/utils.ts b/src/legacy/core_plugins/data/public/search/aggs/utils.ts index 67ea373f438fb8..62f07ce44ab46e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/utils.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/utils.ts @@ -26,7 +26,7 @@ import { isValidEsInterval } from '../../../common'; * @param {string} value a string that should be validated * @returns {boolean} true if value is a valid JSON or if value is an empty string, or a string with whitespaces, otherwise false */ -export function isValidJson(value: string): boolean { +function isValidJson(value: string): boolean { if (!value || value.length === 0) { return true; } @@ -49,7 +49,7 @@ export function isValidJson(value: string): boolean { } } -export function isValidInterval(value: string, baseInterval?: string) { +function isValidInterval(value: string, baseInterval?: string) { if (baseInterval) { return _parseWithBase(value, baseInterval); } else { @@ -69,37 +69,4 @@ function _parseWithBase(value: string, baseInterval: string) { } } -// An inlined version of angular.toJSON() -// source: https://github.com/angular/angular.js/blob/master/src/Angular.js#L1312 -// @internal -export function toAngularJSON(obj: any, pretty?: any): string { - if (obj === undefined) return ''; - if (typeof pretty === 'number') { - pretty = pretty ? 2 : null; - } - return JSON.stringify(obj, toJsonReplacer, pretty); -} - -function isWindow(obj: any) { - return obj && obj.window === obj; -} - -function isScope(obj: any) { - return obj && obj.$evalAsync && obj.$watch; -} - -function toJsonReplacer(key: any, value: any) { - let val = value; - - if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { - val = undefined; - } else if (isWindow(value)) { - val = '$WINDOW'; - } else if (value && window.document === value) { - val = '$DOCUMENT'; - } else if (isScope(value)) { - val = '$SCOPE'; - } - - return val; -} +export { isValidJson, isValidInterval }; 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 24dd1c4944bfba..7a5d927d0f219a 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -19,7 +19,7 @@ import { get, has } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { createAggConfigs, IAggConfigs } from 'ui/agg_types'; +import { AggConfigs, IAggConfigs } from 'ui/agg_types'; import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { KibanaContext, @@ -258,7 +258,7 @@ export const esaggs = (): ExpressionFunctionDefinition { - const { aggs } = getSearchServiceShim(); - const aggConfigs = aggs.createAggConfigs(indexPattern); + const aggConfigs = new AggConfigs(indexPattern); const aggConfig = aggConfigs.createAggConfig({ enabled: true, type, diff --git a/src/legacy/core_plugins/data/public/search/mocks.ts b/src/legacy/core_plugins/data/public/search/mocks.ts deleted file mode 100644 index 86b6a928dc5b42..00000000000000 --- a/src/legacy/core_plugins/data/public/search/mocks.ts +++ /dev/null @@ -1,85 +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 { SearchSetup, SearchStart } from './search_service'; -import { AggTypesRegistrySetup, AggTypesRegistryStart } from './aggs/agg_types_registry'; -import { AggConfigs } from './aggs/agg_configs'; -import { mockAggTypesRegistry } from './aggs/test_helpers'; - -const aggTypeBaseParamMock = () => ({ - name: 'some_param', - type: 'some_param_type', - displayName: 'some_agg_type_param', - required: false, - advanced: false, - default: {}, - write: jest.fn(), - serialize: jest.fn().mockImplementation(() => {}), - deserialize: jest.fn().mockImplementation(() => {}), - options: [], -}); - -const aggTypeConfigMock = () => ({ - name: 'some_name', - title: 'some_title', - params: [aggTypeBaseParamMock()], -}); - -export const aggTypesRegistrySetupMock = (): MockedKeys => ({ - registerBucket: jest.fn(), - registerMetric: jest.fn(), -}); - -export const aggTypesRegistryStartMock = (): MockedKeys => ({ - get: jest.fn().mockImplementation(aggTypeConfigMock), - getBuckets: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), - getMetrics: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), - getAll: jest.fn().mockImplementation(() => ({ - buckets: [aggTypeConfigMock()], - metrics: [aggTypeConfigMock()], - })), -}); - -export const searchSetupMock = (): MockedKeys => ({ - aggs: { - types: aggTypesRegistrySetupMock(), - }, -}); - -export const searchStartMock = (): MockedKeys => ({ - aggs: { - createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { - return new AggConfigs(indexPattern, configStates, { - schemas, - typesRegistry: mockAggTypesRegistry(), - }); - }), - types: mockAggTypesRegistry(), - __LEGACY: { - AggConfig: jest.fn() as any, - AggType: jest.fn(), - aggTypeFieldFilters: jest.fn() as any, - FieldParamType: jest.fn(), - MetricAggType: jest.fn(), - parentPipelineAggHelper: jest.fn() as any, - setBounds: jest.fn(), - siblingPipelineAggHelper: jest.fn() as any, - }, - }, -}); diff --git a/src/legacy/core_plugins/data/public/search/search_service.ts b/src/legacy/core_plugins/data/public/search/search_service.ts index 6754c0e3551af5..45f9ff17328adb 100644 --- a/src/legacy/core_plugins/data/public/search/search_service.ts +++ b/src/legacy/core_plugins/data/public/search/search_service.ts @@ -18,16 +18,11 @@ */ import { CoreSetup, CoreStart } from '../../../../../core/public'; -import { IndexPattern } from '../../../../../plugins/data/public'; import { aggTypes, AggType, - AggTypesRegistry, - AggTypesRegistrySetup, - AggTypesRegistryStart, AggConfig, AggConfigs, - CreateAggConfigParams, FieldParamType, MetricAggType, aggTypeFieldFilters, @@ -37,28 +32,20 @@ import { } from './aggs'; interface AggsSetup { - types: AggTypesRegistrySetup; + types: typeof aggTypes; } -interface AggsStartLegacy { +interface AggsStart { + types: typeof aggTypes; AggConfig: typeof AggConfig; + AggConfigs: typeof AggConfigs; AggType: typeof AggType; aggTypeFieldFilters: typeof aggTypeFieldFilters; FieldParamType: typeof FieldParamType; MetricAggType: typeof MetricAggType; parentPipelineAggHelper: typeof parentPipelineAggHelper; - setBounds: typeof setBounds; siblingPipelineAggHelper: typeof siblingPipelineAggHelper; -} - -interface AggsStart { - createAggConfigs: ( - indexPattern: IndexPattern, - configStates?: CreateAggConfigParams[], - schemas?: Record - ) => InstanceType; - types: AggTypesRegistryStart; - __LEGACY: AggsStartLegacy; + setBounds: typeof setBounds; } export interface SearchSetup { @@ -76,41 +63,28 @@ export interface SearchStart { * it will move into the existing search service in src/plugins/data/public/search */ export class SearchService { - private readonly aggTypesRegistry = new AggTypesRegistry(); - public setup(core: CoreSetup): SearchSetup { - const aggTypesSetup = this.aggTypesRegistry.setup(); - aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b)); - aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m)); - return { aggs: { - types: aggTypesSetup, + types: aggTypes, // TODO convert to registry + // TODO add other items as needed }, }; } public start(core: CoreStart): SearchStart { - const aggTypesStart = this.aggTypesRegistry.start(); return { aggs: { - createAggConfigs: (indexPattern, configStates = [], schemas) => { - return new AggConfigs(indexPattern, configStates, { - schemas, - typesRegistry: aggTypesStart, - }); - }, - types: aggTypesStart, - __LEGACY: { - AggConfig, // TODO make static - AggType, - aggTypeFieldFilters, - FieldParamType, - MetricAggType, - parentPipelineAggHelper, // TODO make static - setBounds, // TODO make static - siblingPipelineAggHelper, // TODO make static - }, + types: aggTypes, // TODO convert to registry + AggConfig, // TODO make static + AggConfigs, + AggType, + aggTypeFieldFilters, + FieldParamType, + MetricAggType, + parentPipelineAggHelper, // TODO make static + siblingPipelineAggHelper, // TODO make static + setBounds, // TODO make static }, }; } diff --git a/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts index 98048cb25db2fe..ef2748102623ab 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts @@ -20,6 +20,8 @@ import { TabifyBuckets } from './buckets'; import { AggGroupNames } from '../aggs'; +jest.mock('ui/new_platform'); + describe('Buckets wrapper', () => { const check = (aggResp: any, count: number, keys: string[]) => { test('reads the length', () => { diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts index 6c5dc790ef9767..cfd4cd7de640b2 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -18,17 +18,12 @@ */ import { tabifyGetColumns } from './get_columns'; -import { TabbedAggColumn } from './types'; import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; -import { mockAggTypesRegistry, mockDataServices } from '../aggs/test_helpers'; - -describe('get columns', () => { - beforeEach(() => { - mockDataServices(); - }); +import { TabbedAggColumn } from './types'; - const typesRegistry = mockAggTypesRegistry(); +jest.mock('ui/new_platform'); +describe('get columns', () => { const createAggConfigs = (aggs: any[] = []) => { const field = { name: '@timestamp', @@ -43,17 +38,18 @@ describe('get columns', () => { }, } as any; - return new AggConfigs(indexPattern, aggs, { - typesRegistry, - schemas: new Schemas([ + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ { group: AggGroupNames.Metrics, name: 'metric', min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, - ]).all, - }); + ]).all + ); }; test('should inject a count metric if no aggs exist', () => { diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts index 94301eedac74a7..f5df0a683ca00c 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -19,19 +19,14 @@ import { TabbedAggResponseWriter } from './response_writer'; import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; -import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; import { TabbedResponseWriterOptions } from './types'; -describe('TabbedAggResponseWriter class', () => { - beforeEach(() => { - mockDataServices(); - }); +jest.mock('ui/new_platform'); +describe('TabbedAggResponseWriter class', () => { let responseWriter: TabbedAggResponseWriter; - const typesRegistry = mockAggTypesRegistry(); - const splitAggConfig = [ { type: BUCKET_TYPES.TERMS, @@ -71,17 +66,18 @@ describe('TabbedAggResponseWriter class', () => { } as any; return new TabbedAggResponseWriter( - new AggConfigs(indexPattern, aggs, { - typesRegistry, - schemas: new Schemas([ + new AggConfigs( + indexPattern, + aggs, + new Schemas([ { group: AggGroupNames.Metrics, name: 'metric', min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, - ]).all, - }), + ]).all + ), { metricsAtAllLevels: false, partialRows: false, diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts index db4ad3bdea96bd..13fe7719b0a856 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -20,12 +20,11 @@ import { IndexPattern } from '../../../../../../plugins/data/public'; import { tabifyAggResponse } from './tabify'; import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; -import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; -describe('tabifyAggResponse Integration', () => { - const typesRegistry = mockAggTypesRegistry(); +jest.mock('ui/new_platform'); +describe('tabifyAggResponse Integration', () => { const createAggConfigs = (aggs: IAggConfig[] = []) => { const field = { name: '@timestamp', @@ -40,17 +39,18 @@ describe('tabifyAggResponse Integration', () => { }, } as unknown) as IndexPattern; - return new AggConfigs(indexPattern, aggs, { - typesRegistry, - schemas: new Schemas([ + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ { group: AggGroupNames.Metrics, name: 'metric', min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, - ]).all, - }); + ]).all + ); }; const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index 6ae4e415f8caa8..6591aa5fb53d5f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -20,8 +20,7 @@ import { cloneDeep } from 'lodash'; import { Vis, VisState } from 'src/legacy/core_plugins/visualizations/public'; - -import { createAggConfigs, IAggConfig, AggGroupNames } from '../../../legacy_imports'; +import { AggConfigs, IAggConfig, AggGroupNames } from '../../../legacy_imports'; import { EditorStateActionTypes } from './constants'; import { getEnabledMetricAggsCount } from '../../agg_group_helper'; import { EditorAction } from './actions'; @@ -33,8 +32,7 @@ function initEditorState(vis: Vis) { function editorStateReducer(state: VisState, action: EditorAction): VisState { switch (action.type) { case EditorStateActionTypes.ADD_NEW_AGG: { - const payloadAggConfig = action.payload as IAggConfig; - const aggConfig = state.aggs.createAggConfig(payloadAggConfig, { + const aggConfig = state.aggs.createAggConfig(action.payload as IAggConfig, { addToAggConfigs: false, }); aggConfig.brandNew = true; @@ -42,7 +40,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -65,7 +63,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -90,7 +88,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -131,7 +129,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -143,7 +141,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -165,7 +163,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } diff --git a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts index 8aed263c4e4d1d..832f73752a99b6 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts @@ -22,12 +22,12 @@ export { AggType, IAggType, IAggConfig, + AggConfigs, IAggConfigs, AggParam, AggGroupNames, aggGroupNamesMap, aggTypes, - createAggConfigs, FieldParamType, IFieldParamType, BUCKET_TYPES, diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts index 736152c7014dc2..0e1e48d00a1b27 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts @@ -34,7 +34,7 @@ import { stubFields } from '../../../../plugins/data/public/stubs'; import { tableVisResponseHandler } from './table_vis_response_handler'; import { coreMock } from '../../../../core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createAggConfigs } from 'ui/agg_types'; +import { AggConfigs } from 'ui/agg_types'; import { tabifyAggResponse, IAggConfig } from './legacy_imports'; jest.mock('ui/new_platform'); @@ -113,7 +113,7 @@ describe('Table Vis - Controller', () => { return ({ type: tableVisTypeDefinition, params: Object.assign({}, tableVisTypeDefinition.visConfig.defaults, params), - aggs: createAggConfigs( + aggs: new AggConfigs( stubIndexPattern, [ { type: 'count', schema: 'metric' }, diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index 0a3b1938436c0e..fb7a157b53a9ac 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -18,10 +18,10 @@ */ export { + AggConfigs, IAggConfig, IAggConfigs, isDateHistogramBucketAggConfig, setBounds, } from '../../data/public'; -export { createAggConfigs } from 'ui/agg_types'; export { createSavedSearchesLoader } from '../../../../plugins/discover/public'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js index 15a826cc6ddbe5..2f36322c67256a 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js @@ -30,7 +30,7 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; import { PersistedState } from '../../../../../../../src/plugins/visualizations/public'; -import { createAggConfigs } from '../../legacy_imports'; +import { AggConfigs } from '../../legacy_imports'; import { updateVisualizationConfig } from './legacy/vis_update'; import { getTypes } from './services'; @@ -83,7 +83,7 @@ class VisImpl extends EventEmitter { updateVisualizationConfig(state.params, this.params); if (state.aggs || !this.aggs) { - this.aggs = createAggConfigs( + this.aggs = new AggConfigs( this.indexPattern, state.aggs ? state.aggs.aggs || state.aggs : [], this.type.schemas.all @@ -125,7 +125,7 @@ class VisImpl extends EventEmitter { copyCurrentState(includeDisabled = false) { const state = this.getCurrentState(includeDisabled); - state.aggs = createAggConfigs( + state.aggs = new AggConfigs( this.indexPattern, state.aggs.aggs || state.aggs, this.type.schemas.all diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ffc300251c4bb3..ac5d0bed7ef153 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -27,19 +27,18 @@ import { start as dataStart } from '../../../core_plugins/data/public/legacy'; // runtime contracts -const { types } = dataStart.search.aggs; -export const aggTypes = types.getAll(); -export const { createAggConfigs } = dataStart.search.aggs; export const { + types: aggTypes, AggConfig, + AggConfigs, AggType, aggTypeFieldFilters, FieldParamType, MetricAggType, parentPipelineAggHelper, - setBounds, siblingPipelineAggHelper, -} = dataStart.search.aggs.__LEGACY; + setBounds, +} = dataStart.search.aggs; // types export { diff --git a/src/legacy/ui/public/vis/__tests__/_agg_config.js b/src/legacy/ui/public/vis/__tests__/_agg_config.js new file mode 100644 index 00000000000000..9e53044f681baa --- /dev/null +++ b/src/legacy/ui/public/vis/__tests__/_agg_config.js @@ -0,0 +1,485 @@ +/* + * 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 sinon from 'sinon'; +import expect from '@kbn/expect'; +import ngMock from 'ng_mock'; +import { AggType, AggConfig } from '../../agg_types'; +import { start as visualizationsStart } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; + +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +describe('AggConfig', function() { + let indexPattern; + + beforeEach(ngMock.module('kibana')); + beforeEach( + ngMock.inject(function(Private) { + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + }) + ); + + describe('#toDsl', function() { + it('calls #write()', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }); + + const aggConfig = vis.aggs.byName('date_histogram')[0]; + const stub = sinon.stub(aggConfig, 'write').returns({ params: {} }); + + aggConfig.toDsl(); + expect(stub.callCount).to.be(1); + }); + + it('uses the type name as the agg name', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }); + + const aggConfig = vis.aggs.byName('date_histogram')[0]; + sinon.stub(aggConfig, 'write').returns({ params: {} }); + + const dsl = aggConfig.toDsl(); + expect(dsl).to.have.property('date_histogram'); + }); + + it('uses the params from #write() output as the agg params', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }); + + const aggConfig = vis.aggs.byName('date_histogram')[0]; + const football = {}; + + sinon.stub(aggConfig, 'write').returns({ params: football }); + + const dsl = aggConfig.toDsl(); + expect(dsl.date_histogram).to.be(football); + }); + + it('includes subAggs from #write() output', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'avg', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }); + + const histoConfig = vis.aggs.byName('date_histogram')[0]; + const avgConfig = vis.aggs.byName('avg')[0]; + const football = {}; + + sinon.stub(histoConfig, 'write').returns({ params: {}, subAggs: [avgConfig] }); + sinon.stub(avgConfig, 'write').returns({ params: football }); + + const dsl = histoConfig.toDsl(); + + // didn't use .eql() because of variable key names, and final check is strict + expect(dsl).to.have.property('aggs'); + expect(dsl.aggs).to.have.property(avgConfig.id); + expect(dsl.aggs[avgConfig.id]).to.have.property('avg'); + expect(dsl.aggs[avgConfig.id].avg).to.be(football); + }); + }); + + describe('::ensureIds', function() { + it('accepts an array of objects and assigns ids to them', function() { + const objs = [{}, {}, {}, {}]; + AggConfig.ensureIds(objs); + expect(objs[0]).to.have.property('id', '1'); + expect(objs[1]).to.have.property('id', '2'); + expect(objs[2]).to.have.property('id', '3'); + expect(objs[3]).to.have.property('id', '4'); + }); + + it('assigns ids relative to the other only item in the list', function() { + const objs = [{ id: '100' }, {}]; + AggConfig.ensureIds(objs); + expect(objs[0]).to.have.property('id', '100'); + expect(objs[1]).to.have.property('id', '101'); + }); + + it('assigns ids relative to the other items in the list', function() { + const objs = [{ id: '100' }, { id: '200' }, { id: '500' }, { id: '350' }, {}]; + AggConfig.ensureIds(objs); + expect(objs[0]).to.have.property('id', '100'); + expect(objs[1]).to.have.property('id', '200'); + expect(objs[2]).to.have.property('id', '500'); + expect(objs[3]).to.have.property('id', '350'); + expect(objs[4]).to.have.property('id', '501'); + }); + + it('uses ::nextId to get the starting value', function() { + sinon.stub(AggConfig, 'nextId').returns(534); + const objs = AggConfig.ensureIds([{}]); + AggConfig.nextId.restore(); + expect(objs[0]).to.have.property('id', '534'); + }); + + it('only calls ::nextId once', function() { + const start = 420; + sinon.stub(AggConfig, 'nextId').returns(start); + const objs = AggConfig.ensureIds([{}, {}, {}, {}, {}, {}, {}]); + + expect(AggConfig.nextId).to.have.property('callCount', 1); + + AggConfig.nextId.restore(); + objs.forEach(function(obj, i) { + expect(obj).to.have.property('id', String(start + i)); + }); + }); + }); + + describe('::nextId', function() { + it('accepts a list of objects and picks the next id', function() { + const next = AggConfig.nextId([{ id: 100 }, { id: 500 }]); + expect(next).to.be(501); + }); + + it('handles an empty list', function() { + const next = AggConfig.nextId([]); + expect(next).to.be(1); + }); + + it('fails when the list is not defined', function() { + expect(function() { + AggConfig.nextId(); + }).to.throwError(); + }); + }); + + describe('#toJsonDataEquals', function() { + const testsIdentical = [ + { + type: 'metric', + aggs: [ + { + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + ], + }, + { + type: 'histogram', + aggs: [ + { + type: 'avg', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }, + ]; + + testsIdentical.forEach((visConfig, index) => { + it(`identical aggregations (${index})`, function() { + const vis1 = new visualizationsStart.Vis(indexPattern, visConfig); + const vis2 = new visualizationsStart.Vis(indexPattern, visConfig); + expect(vis1.aggs.jsonDataEquals(vis2.aggs.aggs)).to.be(true); + }); + }); + + const testsIdenticalDifferentOrder = [ + { + config1: { + type: 'histogram', + aggs: [ + { + type: 'avg', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }, + config2: { + type: 'histogram', + aggs: [ + { + schema: 'metric', + type: 'avg', + }, + { + schema: 'segment', + type: 'date_histogram', + }, + ], + }, + }, + ]; + + testsIdenticalDifferentOrder.forEach((test, index) => { + it(`identical aggregations (${index}) - init json is in different order`, function() { + const vis1 = new visualizationsStart.Vis(indexPattern, test.config1); + const vis2 = new visualizationsStart.Vis(indexPattern, test.config2); + expect(vis1.aggs.jsonDataEquals(vis2.aggs.aggs)).to.be(true); + }); + }); + + const testsDifferent = [ + { + config1: { + type: 'histogram', + aggs: [ + { + type: 'avg', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }, + config2: { + type: 'histogram', + aggs: [ + { + type: 'max', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }, + }, + { + config1: { + type: 'metric', + aggs: [ + { + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + ], + }, + config2: { + type: 'metric', + aggs: [ + { + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }, + }, + ]; + + testsDifferent.forEach((test, index) => { + it(`different aggregations (${index})`, function() { + const vis1 = new visualizationsStart.Vis(indexPattern, test.config1); + const vis2 = new visualizationsStart.Vis(indexPattern, test.config2); + expect(vis1.aggs.jsonDataEquals(vis2.aggs.aggs)).to.be(false); + }); + }); + }); + + describe('#toJSON', function() { + it('includes the aggs id, params, type and schema', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }); + + const aggConfig = vis.aggs.byName('date_histogram')[0]; + expect(aggConfig.id).to.be('1'); + expect(aggConfig.params).to.be.an('object'); + expect(aggConfig.type) + .to.be.an(AggType) + .and.have.property('name', 'date_histogram'); + expect(aggConfig.schema) + .to.be.an('object') + .and.have.property('name', 'segment'); + + const state = aggConfig.toJSON(); + expect(state).to.have.property('id', '1'); + expect(state.params).to.be.an('object'); + expect(state).to.have.property('type', 'date_histogram'); + expect(state).to.have.property('schema', 'segment'); + }); + + it('test serialization order is identical (for visual consistency)', function() { + const vis1 = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + }, + ], + }); + const vis2 = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + schema: 'segment', + type: 'date_histogram', + }, + ], + }); + + //this relies on the assumption that js-engines consistently loop over properties in insertion order. + //most likely the case, but strictly speaking not guaranteed by the JS and JSON specifications. + expect(JSON.stringify(vis1.aggs.aggs) === JSON.stringify(vis2.aggs.aggs)).to.be(true); + }); + }); + + describe('#makeLabel', function() { + it('uses the custom label if it is defined', function() { + const vis = new visualizationsStart.Vis(indexPattern, {}); + const aggConfig = vis.aggs.aggs[0]; + aggConfig.params.customLabel = 'Custom label'; + const label = aggConfig.makeLabel(); + expect(label).to.be(aggConfig.params.customLabel); + }); + it('default label should be "Count"', function() { + const vis = new visualizationsStart.Vis(indexPattern, {}); + const aggConfig = vis.aggs.aggs[0]; + const label = aggConfig.makeLabel(); + expect(label).to.be('Count'); + }); + it('default label should be "Percentage of Count" when percentageMode is set to true', function() { + const vis = new visualizationsStart.Vis(indexPattern, {}); + const aggConfig = vis.aggs.aggs[0]; + const label = aggConfig.makeLabel(true); + expect(label).to.be('Percentage of Count'); + }); + it('empty label if the visualizationsStart.Vis type is not defined', function() { + const vis = new visualizationsStart.Vis(indexPattern, {}); + const aggConfig = vis.aggs.aggs[0]; + aggConfig.type = undefined; + const label = aggConfig.makeLabel(); + expect(label).to.be(''); + }); + }); + + describe('#fieldFormatter - custom getFormat handler', function() { + it('returns formatter from getFormat handler', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'metric', + aggs: [ + { + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + ], + }); + + const fieldFormatter = vis.aggs.aggs[0].fieldFormatter(); + + expect(fieldFormatter).to.be.defined; + expect(fieldFormatter('text')).to.be('text'); + }); + }); + + describe('#fieldFormatter - no custom getFormat handler', function() { + const visStateAggWithoutCustomGetFormat = { + aggs: [ + { + type: 'histogram', + schema: 'bucket', + params: { field: 'bytes' }, + }, + ], + }; + let vis; + + beforeEach(function() { + vis = new visualizationsStart.Vis(indexPattern, visStateAggWithoutCustomGetFormat); + }); + + it("returns the field's formatter", function() { + expect(vis.aggs.aggs[0].fieldFormatter().toString()).to.be( + vis.aggs.aggs[0] + .getField() + .format.getConverterFor() + .toString() + ); + }); + + it('returns the string format if the field does not have a format', function() { + const agg = vis.aggs.aggs[0]; + agg.params.field = { type: 'number', format: null }; + const fieldFormatter = agg.fieldFormatter(); + expect(fieldFormatter).to.be.defined; + expect(fieldFormatter('text')).to.be('text'); + }); + + it('returns the string format if their is no field', function() { + const agg = vis.aggs.aggs[0]; + delete agg.params.field; + const fieldFormatter = agg.fieldFormatter(); + expect(fieldFormatter).to.be.defined; + expect(fieldFormatter('text')).to.be('text'); + }); + + it('returns the html converter if "html" is passed in', function() { + const field = indexPattern.fields.getByName('bytes'); + expect(vis.aggs.aggs[0].fieldFormatter('html').toString()).to.be( + field.format.getConverterFor('html').toString() + ); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/__tests__/_agg_configs.js b/src/legacy/ui/public/vis/__tests__/_agg_configs.js new file mode 100644 index 00000000000000..172523ec50c8b0 --- /dev/null +++ b/src/legacy/ui/public/vis/__tests__/_agg_configs.js @@ -0,0 +1,420 @@ +/* + * 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 sinon from 'sinon'; +import expect from '@kbn/expect'; +import ngMock from 'ng_mock'; +import { AggConfig, AggConfigs, AggGroupNames, Schemas } from '../../agg_types'; +import { start as visualizationsStart } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +describe('AggConfigs', function() { + let indexPattern; + + beforeEach(ngMock.module('kibana')); + beforeEach( + ngMock.inject(function(Private) { + // load main deps + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + }) + ); + + describe('constructor', function() { + it('handles passing just a vis', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [], + }); + + const ac = new AggConfigs(vis.indexPattern, [], vis.type.schemas.all); + expect(ac.aggs).to.have.length(1); + }); + + it('converts configStates into AggConfig objects if they are not already', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [], + }); + + const ac = new AggConfigs( + vis.indexPattern, + [ + { + type: 'date_histogram', + schema: 'segment', + }, + new AggConfig(vis.aggs, { + type: 'terms', + schema: 'split', + }), + ], + vis.type.schemas.all + ); + + expect(ac.aggs).to.have.length(3); + }); + + it('attempts to ensure that all states have an id', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [], + }); + + const states = [ + { + type: 'date_histogram', + schema: 'segment', + }, + { + type: 'terms', + schema: 'split', + }, + ]; + + const spy = sinon.spy(AggConfig, 'ensureIds'); + new AggConfigs(vis.indexPattern, states, vis.type.schemas.all); + expect(spy.callCount).to.be(1); + expect(spy.firstCall.args[0]).to.be(states); + AggConfig.ensureIds.restore(); + }); + + describe('defaults', function() { + let vis; + beforeEach(function() { + vis = { + indexPattern: indexPattern, + type: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: 'Simple', + min: 1, + max: 2, + defaults: [ + { schema: 'metric', type: 'count' }, + { schema: 'metric', type: 'avg' }, + { schema: 'metric', type: 'sum' }, + ], + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: 'Example', + min: 0, + max: 1, + defaults: [ + { schema: 'segment', type: 'terms' }, + { schema: 'segment', type: 'filters' }, + ], + }, + ]), + }, + }; + }); + + it('should only set the number of defaults defined by the max', function() { + const ac = new AggConfigs(vis.indexPattern, [], vis.type.schemas.all); + expect(ac.bySchemaName('metric')).to.have.length(2); + }); + + it('should set the defaults defined in the schema when none exist', function() { + const ac = new AggConfigs(vis.indexPattern, [], vis.type.schemas.all); + expect(ac.aggs).to.have.length(3); + }); + + it('should NOT set the defaults defined in the schema when some exist', function() { + const ac = new AggConfigs( + vis.indexPattern, + [{ schema: 'segment', type: 'date_histogram' }], + vis.type.schemas.all + ); + expect(ac.aggs).to.have.length(3); + expect(ac.bySchemaName('segment')[0].type.name).to.equal('date_histogram'); + }); + }); + }); + + describe('#getRequestAggs', function() { + it('performs a stable sort, but moves metrics to the bottom', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'avg', schema: 'metric' }, + { type: 'terms', schema: 'split' }, + { type: 'histogram', schema: 'split' }, + { type: 'sum', schema: 'metric' }, + { type: 'date_histogram', schema: 'segment' }, + { type: 'filters', schema: 'split' }, + { type: 'percentiles', schema: 'metric' }, + ], + }); + + const sorted = vis.aggs.getRequestAggs(); + const aggs = _.indexBy(vis.aggs.aggs, function(agg) { + return agg.type.name; + }); + + expect(sorted.shift()).to.be(aggs.terms); + expect(sorted.shift()).to.be(aggs.histogram); + expect(sorted.shift()).to.be(aggs.date_histogram); + expect(sorted.shift()).to.be(aggs.filters); + expect(sorted.shift()).to.be(aggs.avg); + expect(sorted.shift()).to.be(aggs.sum); + expect(sorted.shift()).to.be(aggs.percentiles); + expect(sorted).to.have.length(0); + }); + }); + + describe('#getResponseAggs', function() { + it('returns all request aggs for basic aggs', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'terms', schema: 'split' }, + { type: 'date_histogram', schema: 'segment' }, + { type: 'count', schema: 'metric' }, + ], + }); + + const sorted = vis.aggs.getResponseAggs(); + const aggs = _.indexBy(vis.aggs.aggs, function(agg) { + return agg.type.name; + }); + + expect(sorted.shift()).to.be(aggs.terms); + expect(sorted.shift()).to.be(aggs.date_histogram); + expect(sorted.shift()).to.be(aggs.count); + expect(sorted).to.have.length(0); + }); + + it('expands aggs that have multiple responses', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'terms', schema: 'split' }, + { type: 'date_histogram', schema: 'segment' }, + { type: 'percentiles', schema: 'metric', params: { percents: [1, 2, 3] } }, + ], + }); + + const sorted = vis.aggs.getResponseAggs(); + const aggs = _.indexBy(vis.aggs.aggs, function(agg) { + return agg.type.name; + }); + + expect(sorted.shift()).to.be(aggs.terms); + expect(sorted.shift()).to.be(aggs.date_histogram); + expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 1); + expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 2); + expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 3); + expect(sorted).to.have.length(0); + }); + }); + + describe('#toDsl', function() { + it('uses the sorted aggs', function() { + const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram' }); + sinon.spy(vis.aggs, 'getRequestAggs'); + vis.aggs.toDsl(); + expect(vis.aggs.getRequestAggs).to.have.property('callCount', 1); + }); + + it('calls aggConfig#toDsl() on each aggConfig and compiles the nested output', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'date_histogram', schema: 'segment' }, + { type: 'filters', schema: 'split' }, + ], + }); + + const aggInfos = vis.aggs.aggs.map(function(aggConfig) { + const football = {}; + + sinon.stub(aggConfig, 'toDsl').returns(football); + + return { + id: aggConfig.id, + football: football, + }; + }); + + (function recurse(lvl) { + const info = aggInfos.shift(); + + expect(lvl).to.have.property(info.id); + expect(lvl[info.id]).to.be(info.football); + + if (lvl[info.id].aggs) { + return recurse(lvl[info.id].aggs); + } + })(vis.aggs.toDsl()); + + expect(aggInfos).to.have.length(1); + }); + + it("skips aggs that don't have a dsl representation", function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'count', schema: 'metric' }, + ], + }); + + const dsl = vis.aggs.toDsl(); + const histo = vis.aggs.byName('date_histogram')[0]; + const count = vis.aggs.byName('count')[0]; + + expect(dsl).to.have.property(histo.id); + expect(dsl[histo.id]).to.be.an('object'); + expect(dsl[histo.id]).to.not.have.property('aggs'); + expect(dsl).to.not.have.property(count.id); + }); + + it('writes multiple metric aggregations at the same level', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { type: 'min', schema: 'metric', params: { field: 'bytes' } }, + { type: 'max', schema: 'metric', params: { field: 'bytes' } }, + ], + }); + + const dsl = vis.aggs.toDsl(); + + const histo = vis.aggs.byName('date_histogram')[0]; + const metrics = vis.aggs.bySchemaGroup('metrics'); + + expect(dsl).to.have.property(histo.id); + expect(dsl[histo.id]).to.be.an('object'); + expect(dsl[histo.id]).to.have.property('aggs'); + + metrics.forEach(function(metric) { + expect(dsl[histo.id].aggs).to.have.property(metric.id); + expect(dsl[histo.id].aggs[metric.id]).to.not.have.property('aggs'); + }); + }); + + it('writes multiple metric aggregations at every level if the vis is hierarchical', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'terms', schema: 'segment', params: { field: 'ip', orderBy: 1 } }, + { type: 'terms', schema: 'segment', params: { field: 'extension', orderBy: 1 } }, + { id: 1, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { type: 'min', schema: 'metric', params: { field: 'bytes' } }, + { type: 'max', schema: 'metric', params: { field: 'bytes' } }, + ], + }); + vis.isHierarchical = _.constant(true); + + const topLevelDsl = vis.aggs.toDsl(vis.isHierarchical()); + const buckets = vis.aggs.bySchemaGroup('buckets'); + const metrics = vis.aggs.bySchemaGroup('metrics'); + + (function checkLevel(dsl) { + const bucket = buckets.shift(); + expect(dsl).to.have.property(bucket.id); + + expect(dsl[bucket.id]).to.be.an('object'); + expect(dsl[bucket.id]).to.have.property('aggs'); + + metrics.forEach(function(metric) { + expect(dsl[bucket.id].aggs).to.have.property(metric.id); + expect(dsl[bucket.id].aggs[metric.id]).to.not.have.property('aggs'); + }); + + if (buckets.length) { + checkLevel(dsl[bucket.id].aggs); + } + })(topLevelDsl); + }); + + it('adds the parent aggs of nested metrics at every level if the vis is hierarchical', function() { + const vis = new visualizationsStart.Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + id: '1', + type: 'avg_bucket', + schema: 'metric', + params: { + customBucket: { + id: '1-bucket', + type: 'date_histogram', + schema: 'bucketAgg', + params: { + field: '@timestamp', + interval: '10s', + }, + }, + customMetric: { + id: '1-metric', + type: 'count', + schema: 'metricAgg', + params: {}, + }, + }, + }, + { + id: '2', + type: 'terms', + schema: 'bucket', + params: { + field: 'geo.src', + }, + }, + { + id: '3', + type: 'terms', + schema: 'bucket', + params: { + field: 'machine.os', + }, + }, + ], + }); + vis.isHierarchical = _.constant(true); + + const topLevelDsl = vis.aggs.toDsl(vis.isHierarchical())['2']; + expect(topLevelDsl.aggs).to.have.keys(['1', '1-bucket']); + expect(topLevelDsl.aggs['1'].avg_bucket).to.have.property('buckets_path', '1-bucket>_count'); + expect(topLevelDsl.aggs['3'].aggs).to.have.keys(['1', '1-bucket']); + expect(topLevelDsl.aggs['3'].aggs['1'].avg_bucket).to.have.property( + 'buckets_path', + '1-bucket>_count' + ); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/index.ts b/src/legacy/ui/public/vis/__tests__/index.js similarity index 86% rename from src/legacy/core_plugins/data/public/search/aggs/test_helpers/index.ts rename to src/legacy/ui/public/vis/__tests__/index.js index 131f921586144c..46074f2c5197b7 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/legacy/ui/public/vis/__tests__/index.js @@ -17,5 +17,5 @@ * under the License. */ -export { mockAggTypesRegistry } from './mock_agg_types_registry'; -export { mockDataServices } from './mock_data_services'; +import './_agg_config'; +import './_agg_configs'; diff --git a/src/plugins/data/common/field_formats/mocks.ts b/src/plugins/data/common/field_formats/mocks.ts deleted file mode 100644 index bc38374e147cf6..00000000000000 --- a/src/plugins/data/common/field_formats/mocks.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FieldFormat, IFieldFormatsRegistry } from '.'; - -const fieldFormatMock = ({ - convert: jest.fn(), - getConverterFor: jest.fn(), - getParamDefaults: jest.fn(), - param: jest.fn(), - params: jest.fn(), - toJSON: jest.fn(), - type: jest.fn(), - setupContentType: jest.fn(), -} as unknown) as FieldFormat; - -export const fieldFormatsMock: IFieldFormatsRegistry = { - getByFieldType: jest.fn(), - getDefaultConfig: jest.fn(), - getDefaultInstance: jest.fn().mockImplementation(() => fieldFormatMock) as any, - getDefaultInstanceCacheResolver: jest.fn(), - getDefaultInstancePlain: jest.fn(), - getDefaultType: jest.fn(), - getDefaultTypeName: jest.fn(), - getInstance: jest.fn() as any, - getType: jest.fn(), - getTypeNameByEsTypes: jest.fn(), - init: jest.fn(), - register: jest.fn(), - parseDefaultTypeMap: jest.fn(), - deserialize: jest.fn(), - getTypeWithoutMetaParams: jest.fn(), -}; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 27de3b5a29bfd0..6a0a33096eaac1 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -16,9 +16,13 @@ * specific language governing permissions and limitations * under the License. */ - -import { Plugin, DataPublicPluginSetup, DataPublicPluginStart, IndexPatternsContract } from '.'; -import { fieldFormatsMock } from '../common/field_formats/mocks'; +import { + Plugin, + DataPublicPluginSetup, + DataPublicPluginStart, + IndexPatternsContract, + IFieldFormatsRegistry, +} from '.'; import { searchSetupMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; @@ -31,6 +35,24 @@ const autocompleteMock: any = { hasQuerySuggestions: jest.fn(), }; +const fieldFormatsMock: IFieldFormatsRegistry = { + getByFieldType: jest.fn(), + getDefaultConfig: jest.fn(), + getDefaultInstance: jest.fn() as any, + getDefaultInstanceCacheResolver: jest.fn(), + getDefaultInstancePlain: jest.fn(), + getDefaultType: jest.fn(), + getDefaultTypeName: jest.fn(), + getInstance: jest.fn() as any, + getType: jest.fn(), + getTypeNameByEsTypes: jest.fn(), + init: jest.fn(), + register: jest.fn(), + parseDefaultTypeMap: jest.fn(), + deserialize: jest.fn(), + getTypeWithoutMetaParams: jest.fn(), +}; + const createSetupContract = (): Setup => { const querySetupMock = queryServiceMock.createSetupContract(); const setupContract = { diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 700bea741bd6ae..fd72158012de6f 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -17,6 +17,25 @@ * under the License. */ +/* + * 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 { ISearchSource } from './search_source'; export const searchSourceMock: MockedKeys = { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 2e1645c816140c..ef4b5f6d7b834f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -17,6 +17,10 @@ jest.mock('ui/new_platform'); // mock away actual dependencies to prevent all of it being loaded jest.mock('../../../../../../src/legacy/core_plugins/interpreter/public/registries', () => {}); +jest.mock('../../../../../../src/legacy/core_plugins/data/public/legacy', () => ({ + start: {}, + setup: {}, +})); jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, })); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index da04b970a494b8..c883983f8cf010 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -2803,7 +2803,7 @@ tr:hover .c3:focus::before {
- - -
- - -
- - - - - - + -
- - - - - - - - + } + > + - Cu0n232QMyvNtzb75j - - - - + + - - + > + + + + - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
+ + + + + + + +
+ + + + + + + + + + + + + + @@ -3087,7 +3105,7 @@ tr:hover .c3:focus::before {
- - -
- - -
- - - - - - + -
- - - - - - - - + } + > + - files - - - - + + - - + > + + + + - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
+ + + + + + + +
+ + + + + + + + + + + + + + @@ -3371,7 +3407,7 @@ tr:hover .c3:focus::before {
- - -
- - -
- - - - - - + -
- - - - - - - - + } + > + - sha1: fa5195a... - - - - + + - - + > + + + + - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
+ + + + + + + +
+ + + + + + + + + + + + + + @@ -3655,7 +3709,7 @@ tr:hover .c3:focus::before {
- - -
- - -
- - - - - - + -
- - - - - - - - + } + > + - md5: f7653f1... - - - - + + - - + > + + + + - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
+ + + + + + + +
+ + + + + + + + + + + + + + From 8a8af5a57c5fa33e02da8e8d3db652f5b716375e Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 27 Feb 2020 23:37:39 -0700 Subject: [PATCH 25/34] skip flaky suite (#58662) --- .../bfetch/common/buffer/tests/timed_item_buffer.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts b/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts index c1c6a8f187a44c..e1640927c4ead3 100644 --- a/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts +++ b/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts @@ -20,7 +20,8 @@ import { TimedItemBuffer } from '../timed_item_buffer'; import { runItemBufferTests } from './run_item_buffer_tests'; -describe('TimedItemBuffer', () => { +// FLAKY: https://github.com/elastic/kibana/issues/58662 +describe.skip('TimedItemBuffer', () => { runItemBufferTests(TimedItemBuffer); test('does not do unnecessary flushes', async () => { From 913afb3bada2bd75f045ac7d8ec7483c774e2c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 28 Feb 2020 08:54:50 +0100 Subject: [PATCH 26/34] Update APM readme --- x-pack/legacy/plugins/apm/readme.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index a513249c296db3..0edcdc279815cd 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -74,10 +74,14 @@ node scripts/jest.js plugins/apm --updateSnapshot ### Functional tests **Start server** -`node scripts/functional_tests_server --config x-pack/test/functional/config.js` +``` +node scripts/functional_tests_server --config x-pack/test/functional/config.js +``` **Run tests** -`node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs'` +``` +node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs' +``` APM tests are located in `x-pack/test/functional/apps/apm`. For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) @@ -85,10 +89,14 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### API integration tests **Start server** -`node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` +``` +node scripts/functional_tests_server --config x-pack/test/api_integration/config.js +``` **Run tests** -`node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs'` +``` +node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs' +``` APM tests are located in `x-pack/test/api_integration/apis/apm`. For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) From ce45647ea2212f5cc42147e9b5bb82cb962e81ca Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 28 Feb 2020 09:20:51 +0100 Subject: [PATCH 27/34] block SO setup API calls after startup (#58718) --- ...-plugin-server.savedobjectsservicesetup.md | 2 ++ src/core/MIGRATION_EXAMPLES.md | 8 ++++- .../saved_objects_service.test.ts | 30 +++++++++++++++++++ .../saved_objects/saved_objects_service.ts | 15 ++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md index b6f2e7320c48ad..963c4bbeb55153 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md @@ -16,6 +16,8 @@ export interface SavedObjectsServiceSetup When plugins access the Saved Objects client, a new client is created using the factory provided to `setClientFactory` and wrapped by all wrappers registered through `addClientWrapper`. +All the setup APIs will throw if called after the service has started, and therefor cannot be used from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated. + ## Example 1 diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index def83ba177fc9f..2953edb535f47a 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -917,4 +917,10 @@ Would be converted to: ```typescript const migration: SavedObjectMigrationFn = (doc, { log }) => {...} -``` \ No newline at end of file +``` + +### Remarks + +The `registerType` API will throw if called after the service has started, and therefor cannot be used from +legacy plugin code. Legacy plugins should use the legacy savedObjects service and the legacy way to register +saved object types until migrated. \ No newline at end of file diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index a1e2c1e8dbf263..554acf8d43dcb9 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -232,6 +232,36 @@ describe('SavedObjectsService', () => { expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); + it('throws when calling setup APIs once started', async () => { + const coreContext = createCoreContext({ skipMigration: false }); + const soService = new SavedObjectsService(coreContext); + const setup = await soService.setup(createSetupDeps()); + await soService.start({}); + + expect(() => { + setup.setClientFactoryProvider(jest.fn()); + }).toThrowErrorMatchingInlineSnapshot( + `"cannot call \`setClientFactoryProvider\` after service startup."` + ); + + expect(() => { + setup.addClientWrapper(0, 'dummy', jest.fn()); + }).toThrowErrorMatchingInlineSnapshot( + `"cannot call \`addClientWrapper\` after service startup."` + ); + + expect(() => { + setup.registerType({ + name: 'someType', + hidden: false, + namespaceAgnostic: false, + mappings: { properties: {} }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"cannot call \`registerType\` after service startup."` + ); + }); + describe('#getTypeRegistry', () => { it('returns the internal type registry of the service', async () => { const coreContext = createCoreContext({ skipMigration: false }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index da8f7ab96d6891..d5a9f474209711 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -61,6 +61,9 @@ import { registerRoutes } from './routes'; * the factory provided to `setClientFactory` and wrapped by all wrappers * registered through `addClientWrapper`. * + * All the setup APIs will throw if called after the service has started, and therefor cannot be used + * from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated. + * * @example * ```ts * import { SavedObjectsClient, CoreSetup } from 'src/core/server'; @@ -275,6 +278,7 @@ export class SavedObjectsService private migrator$ = new Subject(); private typeRegistry = new SavedObjectTypeRegistry(); private validations: PropertyValidators = {}; + private started = false; constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('savedobjects-service'); @@ -316,12 +320,18 @@ export class SavedObjectsService return { setClientFactoryProvider: provider => { + if (this.started) { + throw new Error('cannot call `setClientFactoryProvider` after service startup.'); + } if (this.clientFactoryProvider) { throw new Error('custom client factory is already set, and can only be set once'); } this.clientFactoryProvider = provider; }, addClientWrapper: (priority, id, factory) => { + if (this.started) { + throw new Error('cannot call `addClientWrapper` after service startup.'); + } this.clientFactoryWrappers.push({ priority, id, @@ -329,6 +339,9 @@ export class SavedObjectsService }); }, registerType: type => { + if (this.started) { + throw new Error('cannot call `registerType` after service startup.'); + } this.typeRegistry.registerType(type); }, }; @@ -415,6 +428,8 @@ export class SavedObjectsService clientProvider.addClientWrapperFactory(priority, id, factory); }); + this.started = true; + return { migrator, clientProvider, From bd92b7d56f85bd7c788a3d256a9740890ac5f317 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 28 Feb 2020 10:09:41 +0100 Subject: [PATCH 28/34] Implement HTTP Authentication provider and allow `ApiKey` authentication by default. (#58126) --- .../routes/api/__fixtures__/authc_mock.ts | 41 +- x-pack/plugins/case/server/services/index.ts | 10 +- .../authentication/authenticator.test.ts | 329 +++-- .../server/authentication/authenticator.ts | 50 + .../get_http_authentication_scheme.test.ts | 58 + .../get_http_authentication_scheme.ts | 21 + .../server/authentication/index.mock.ts | 3 +- .../server/authentication/index.test.ts | 2 +- .../security/server/authentication/index.ts | 1 + .../authentication/providers/base.mock.ts | 30 - .../server/authentication/providers/base.ts | 7 + .../authentication/providers/basic.test.ts | 227 ++-- .../server/authentication/providers/basic.ts | 67 +- .../authentication/providers/http.test.ts | 198 +++ .../server/authentication/providers/http.ts | 99 ++ .../server/authentication/providers/index.ts | 1 + .../authentication/providers/kerberos.test.ts | 612 ++++----- .../authentication/providers/kerberos.ts | 63 +- .../authentication/providers/oidc.test.ts | 603 +++++---- .../server/authentication/providers/oidc.ts | 62 +- .../authentication/providers/pki.test.ts | 352 ++--- .../server/authentication/providers/pki.ts | 48 +- .../authentication/providers/saml.test.ts | 1163 ++++++++--------- .../server/authentication/providers/saml.ts | 51 +- .../authentication/providers/token.test.ts | 541 ++++---- .../server/authentication/providers/token.ts | 59 +- x-pack/plugins/security/server/config.test.ts | 198 +-- x-pack/plugins/security/server/config.ts | 5 + x-pack/plugins/security/server/index.ts | 11 + x-pack/plugins/security/server/plugin.test.ts | 2 + .../routes/authentication/basic.test.ts | 18 + .../server/routes/authentication/basic.ts | 13 +- .../server/routes/authentication/index.ts | 9 +- .../api_integration/apis/security/session.ts | 8 +- 34 files changed, 2579 insertions(+), 2383 deletions(-) create mode 100644 x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts create mode 100644 x-pack/plugins/security/server/authentication/get_http_authentication_scheme.ts create mode 100644 x-pack/plugins/security/server/authentication/providers/http.test.ts create mode 100644 x-pack/plugins/security/server/authentication/providers/http.ts diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts index 94ce9627b9ac64..17a25184826378 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts @@ -3,33 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Authentication } from '../../../../../security/server'; +import { AuthenticatedUser } from '../../../../../security/server'; +import { securityMock } from '../../../../../security/server/mocks'; -const getCurrentUser = jest.fn().mockReturnValue({ - username: 'awesome', - full_name: 'Awesome D00d', -}); -const getCurrentUserThrow = jest.fn().mockImplementation(() => { - throw new Error('Bad User - the user is not authenticated'); -}); +function createAuthenticationMock({ + currentUser, +}: { currentUser?: AuthenticatedUser | null } = {}) { + const { authc } = securityMock.createSetup(); + authc.getCurrentUser.mockReturnValue( + currentUser !== undefined + ? currentUser + : ({ username: 'awesome', full_name: 'Awesome D00d' } as AuthenticatedUser) + ); + return authc; +} export const authenticationMock = { - create: (): jest.Mocked => ({ - login: jest.fn(), - createAPIKey: jest.fn(), - getCurrentUser, - invalidateAPIKey: jest.fn(), - isAuthenticated: jest.fn(), - logout: jest.fn(), - getSessionInfo: jest.fn(), - }), - createInvalid: (): jest.Mocked => ({ - login: jest.fn(), - createAPIKey: jest.fn(), - getCurrentUser: getCurrentUserThrow, - invalidateAPIKey: jest.fn(), - isAuthenticated: jest.fn(), - logout: jest.fn(), - getSessionInfo: jest.fn(), - }), + create: () => createAuthenticationMock(), + createInvalid: () => createAuthenticationMock({ currentUser: null }), }; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index d6d4bd606676c8..e6416e268e30bf 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -149,14 +149,8 @@ export class CaseService { } }, getUser: async ({ request, response }: GetUserArgs) => { - let user; - try { - this.log.debug(`Attempting to authenticate a user`); - user = await authentication!.getCurrentUser(request); - } catch (error) { - this.log.debug(`Error on GET user: ${error}`); - throw error; - } + this.log.debug(`Attempting to authenticate a user`); + const user = authentication!.getCurrentUser(request); if (!user) { this.log.debug(`Error on GET user: Bad User`); throw new Error('Bad User - the user is not authenticated'); diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 65874ba3a461e6..af019ff10dedc3 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -5,6 +5,8 @@ */ jest.mock('./providers/basic'); +jest.mock('./providers/saml'); +jest.mock('./providers/http'); import Boom from 'boom'; import { duration, Duration } from 'moment'; @@ -23,15 +25,27 @@ import { Authenticator, AuthenticatorOptions, ProviderSession } from './authenti import { DeauthenticationResult } from './deauthentication_result'; import { BasicAuthenticationProvider } from './providers'; -function getMockOptions(config: Partial = {}) { +function getMockOptions({ + session, + providers, + http = {}, +}: { + session?: AuthenticatorOptions['config']['session']; + providers?: string[]; + http?: Partial; +} = {}) { return { clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), config: { - session: { idleTimeout: null, lifespan: null }, - authc: { providers: [], oidc: {}, saml: {} }, - ...config, + session: { idleTimeout: null, lifespan: null, ...(session || {}) }, + authc: { + providers: providers || [], + oidc: {}, + saml: {}, + http: { enabled: true, autoSchemesEnabled: true, schemes: ['apikey'], ...http }, + }, }, sessionStorageFactory: sessionStorageMock.createFactory(), }; @@ -44,8 +58,13 @@ describe('Authenticator', () => { login: jest.fn(), authenticate: jest.fn(), logout: jest.fn(), + getHTTPAuthenticationScheme: jest.fn(), }; + jest.requireMock('./providers/http').HTTPAuthenticationProvider.mockImplementation(() => ({ + authenticate: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()), + })); + jest .requireMock('./providers/basic') .BasicAuthenticationProvider.mockImplementation(() => mockBasicAuthenticationProvider); @@ -55,23 +74,80 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ - authc: { providers: [], oidc: {}, saml: {} }, - }); - expect(() => new Authenticator(mockOptions)).toThrowError( + expect(() => new Authenticator(getMockOptions())).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); }); it('fails if configured authentication provider is not known.', () => { - const mockOptions = getMockOptions({ - authc: { providers: ['super-basic'], oidc: {}, saml: {} }, - }); - - expect(() => new Authenticator(mockOptions)).toThrowError( + expect(() => new Authenticator(getMockOptions({ providers: ['super-basic'] }))).toThrowError( 'Unsupported authentication provider name: super-basic.' ); }); + + describe('HTTP authentication provider', () => { + beforeEach(() => { + jest + .requireMock('./providers/basic') + .BasicAuthenticationProvider.mockImplementation(() => ({ + getHTTPAuthenticationScheme: jest.fn().mockReturnValue('basic'), + })); + }); + + afterEach(() => jest.resetAllMocks()); + + it('enabled by default', () => { + const authenticator = new Authenticator(getMockOptions({ providers: ['basic'] })); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(true); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'basic']), + }); + }); + + it('includes all required schemes if `autoSchemesEnabled` is enabled', () => { + const authenticator = new Authenticator( + getMockOptions({ providers: ['basic', 'kerberos'] }) + ); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('kerberos')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(true); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'basic', 'bearer']), + }); + }); + + it('does not include additional schemes if `autoSchemesEnabled` is disabled', () => { + const authenticator = new Authenticator( + getMockOptions({ providers: ['basic', 'kerberos'], http: { autoSchemesEnabled: false } }) + ); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('kerberos')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(true); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { supportedSchemes: new Set(['apikey']) }); + }); + + it('disabled if explicitly disabled', () => { + const authenticator = new Authenticator( + getMockOptions({ providers: ['basic'], http: { enabled: false } }) + ); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(false); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).not.toHaveBeenCalled(); + }); + }); }); describe('`login` method', () => { @@ -80,9 +156,7 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { @@ -124,12 +198,9 @@ describe('Authenticator', () => { AuthenticationResult.failed(failureReason) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); }); it('returns user that authentication provider returns.', async () => { @@ -140,13 +211,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Basic .....' }); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) + ); }); it('creates session whenever authentication provider returns state', async () => { @@ -158,12 +225,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: { authorization } }) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: { authorization } }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -174,11 +238,9 @@ describe('Authenticator', () => { it('returns `notHandled` if login attempt is targeted to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await authenticator.login(request, { - provider: 'token', - value: {}, - }); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.login(request, { provider: 'token', value: {} })).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); it('clears session if it belongs to a different provider.', async () => { @@ -189,12 +251,9 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: credentials, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toBe(user); + await expect( + authenticator.login(request, { provider: 'basic', value: credentials }) + ).resolves.toEqual(AuthenticationResult.succeeded(user)); expect(mockBasicAuthenticationProvider.login).toHaveBeenCalledWith( request, @@ -214,12 +273,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: null }) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: null }) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -232,9 +288,7 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { @@ -277,10 +331,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) ); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Basic .....' }); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) + ); }); it('creates session whenever authentication provider returns state for system API requests', async () => { @@ -294,9 +347,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: { authorization } }) ); - const systemAPIAuthenticationResult = await authenticator.authenticate(request); - expect(systemAPIAuthenticationResult.succeeded()).toBe(true); - expect(systemAPIAuthenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: { authorization } }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -316,9 +369,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: { authorization } }) ); - const systemAPIAuthenticationResult = await authenticator.authenticate(request); - expect(systemAPIAuthenticationResult.succeeded()).toBe(true); - expect(systemAPIAuthenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: { authorization } }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -338,9 +391,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -357,9 +410,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith(mockSessVal); @@ -377,7 +430,7 @@ describe('Authenticator', () => { idleTimeout: duration(3600 * 24), lifespan: null, }, - authc: { providers: ['basic'], oidc: {}, saml: {} }, + providers: ['basic'], }); mockSessionStorage = sessionStorageMock.create(); @@ -392,9 +445,9 @@ describe('Authenticator', () => { jest.spyOn(Date, 'now').mockImplementation(() => currentDate); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -416,7 +469,7 @@ describe('Authenticator', () => { idleTimeout: duration(hr * 2), lifespan: duration(hr * 8), }, - authc: { providers: ['basic'], oidc: {}, saml: {} }, + providers: ['basic'], }); mockSessionStorage = sessionStorageMock.create(); @@ -437,9 +490,9 @@ describe('Authenticator', () => { jest.spyOn(Date, 'now').mockImplementation(() => currentDate); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -468,7 +521,7 @@ describe('Authenticator', () => { idleTimeout: null, lifespan, }, - authc: { providers: ['basic'], oidc: {}, saml: {} }, + providers: ['basic'], }); mockSessionStorage = sessionStorageMock.create(); @@ -485,9 +538,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user) ); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -517,13 +570,15 @@ describe('Authenticator', () => { headers: { 'kbn-system-request': 'true' }, }); + const failureReason = new Error('some error'); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( - AuthenticationResult.failed(new Error('some error')) + AuthenticationResult.failed(failureReason) ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -534,13 +589,15 @@ describe('Authenticator', () => { headers: { 'kbn-system-request': 'false' }, }); + const failureReason = new Error('some error'); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( - AuthenticationResult.failed(new Error('some error')) + AuthenticationResult.failed(failureReason) ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -558,9 +615,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: newState }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -582,9 +639,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: newState }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -604,8 +661,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -621,8 +679,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -636,8 +695,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.redirected()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo('some-url', { state: null }) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -653,8 +713,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -670,8 +731,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -687,8 +749,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -704,8 +767,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -718,9 +782,7 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { @@ -744,9 +806,10 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockSessionStorage.get.mockResolvedValue(null); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - expect(deauthenticationResult.notHandled()).toBe(true); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); @@ -757,12 +820,12 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('some-url') + ); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockSessionStorage.clear).toHaveBeenCalled(); - expect(deauthenticationResult.redirected()).toBe(true); - expect(deauthenticationResult.redirectURL).toBe('some-url'); }); it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { @@ -773,21 +836,22 @@ describe('Authenticator', () => { DeauthenticationResult.redirectTo('some-url') ); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('some-url') + ); 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); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - expect(deauthenticationResult.notHandled()).toBe(true); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); @@ -796,11 +860,12 @@ describe('Authenticator', () => { const state = { authorization: 'Bearer xxx' }; mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, state, provider: 'token' }); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); - expect(deauthenticationResult.notHandled()).toBe(true); }); }); @@ -809,9 +874,7 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -851,4 +914,16 @@ describe('Authenticator', () => { expect(sessionInfo).toBe(null); }); }); + + describe('`isProviderEnabled` method', () => { + it('returns `true` only if specified provider is enabled', () => { + let authenticator = new Authenticator(getMockOptions({ providers: ['basic'] })); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('saml')).toBe(false); + + authenticator = new Authenticator(getMockOptions({ providers: ['basic', 'saml'] })); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('saml')).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 3ab49d3c5b124b..4954e1b24216cb 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -27,6 +27,7 @@ import { TokenAuthenticationProvider, OIDCAuthenticationProvider, PKIAuthenticationProvider, + HTTPAuthenticationProvider, isSAMLRequestQuery, } from './providers'; import { AuthenticationResult } from './authentication_result'; @@ -191,6 +192,7 @@ export class Authenticator { client: this.options.clusterClient, logger: this.options.loggers.get('tokens'), }), + isProviderEnabled: this.isProviderEnabled.bind(this), }; const authProviders = this.options.config.authc.providers; @@ -206,6 +208,8 @@ export class Authenticator { ? (this.options.config.authc as Record)[providerType] : undefined; + this.logger.debug(`Enabling "${providerType}" authentication provider.`); + return [ providerType, instantiateProvider( @@ -216,6 +220,17 @@ export class Authenticator { ] as [string, BaseAuthenticationProvider]; }) ); + + // For the BWC reasons we always include HTTP authentication provider unless it's explicitly disabled. + if (this.options.config.authc.http.enabled) { + this.setupHTTPAuthenticationProvider( + Object.freeze({ + ...providerCommonOptions, + logger: options.loggers.get(HTTPAuthenticationProvider.type), + }) + ); + } + this.serverBasePath = this.options.basePath.serverBasePath || '/'; this.idleTimeout = this.options.config.session.idleTimeout; @@ -385,6 +400,41 @@ export class Authenticator { return null; } + /** + * Checks whether specified provider type is currently enabled. + * @param providerType Type of the provider (`basic`, `saml`, `pki` etc.). + */ + isProviderEnabled(providerType: string) { + return this.providers.has(providerType); + } + + /** + * Initializes HTTP Authentication provider and appends it to the end of the list of enabled + * authentication providers. + * @param options Common provider options. + */ + private setupHTTPAuthenticationProvider(options: AuthenticationProviderOptions) { + const supportedSchemes = new Set( + this.options.config.authc.http.schemes.map(scheme => scheme.toLowerCase()) + ); + + // If `autoSchemesEnabled` is set we should allow schemes that other providers use to + // authenticate requests with Elasticsearch. + if (this.options.config.authc.http.autoSchemesEnabled) { + for (const provider of this.providers.values()) { + const supportedScheme = provider.getHTTPAuthenticationScheme(); + if (supportedScheme) { + supportedSchemes.add(supportedScheme.toLowerCase()); + } + } + } + + this.providers.set( + HTTPAuthenticationProvider.type, + new HTTPAuthenticationProvider(options, { supportedSchemes }) + ); + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. diff --git a/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts new file mode 100644 index 00000000000000..6a63634394ec0c --- /dev/null +++ b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks'; + +import { getHTTPAuthenticationScheme } from './get_http_authentication_scheme'; + +describe('getHTTPAuthenticationScheme', () => { + it('returns `null` if request does not have authorization header', () => { + expect(getHTTPAuthenticationScheme(httpServerMock.createKibanaRequest())).toBeNull(); + }); + + it('returns `null` if authorization header value isn not a string', () => { + expect( + getHTTPAuthenticationScheme( + httpServerMock.createKibanaRequest({ + headers: { authorization: ['Basic xxx', 'Bearer xxx'] as any }, + }) + ) + ).toBeNull(); + }); + + it('returns `null` if authorization header value is an empty string', () => { + expect( + getHTTPAuthenticationScheme( + httpServerMock.createKibanaRequest({ headers: { authorization: '' } }) + ) + ).toBeNull(); + }); + + it('returns only scheme portion of the authorization header value in lower case', () => { + const headerValueAndSchemeMap = [ + ['Basic xxx', 'basic'], + ['Basic xxx yyy', 'basic'], + ['basic xxx', 'basic'], + ['basic', 'basic'], + // We don't trim leading whitespaces in scheme. + [' Basic xxx', ''], + ['Negotiate xxx', 'negotiate'], + ['negotiate xxx', 'negotiate'], + ['negotiate', 'negotiate'], + ['ApiKey xxx', 'apikey'], + ['apikey xxx', 'apikey'], + ['Api Key xxx', 'api'], + ]; + + for (const [authorization, scheme] of headerValueAndSchemeMap) { + expect( + getHTTPAuthenticationScheme( + httpServerMock.createKibanaRequest({ headers: { authorization } }) + ) + ).toBe(scheme); + } + }); +}); diff --git a/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.ts b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.ts new file mode 100644 index 00000000000000..b9c53f34dbcab7 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.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 { KibanaRequest } from '../../../../../src/core/server'; + +/** + * Parses request's `Authorization` HTTP header if present and extracts authentication scheme. + * https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes + * @param request Request instance to extract authentication scheme for. + */ +export function getHTTPAuthenticationScheme(request: KibanaRequest) { + const authorizationHeaderValue = request.headers.authorization; + if (!authorizationHeaderValue || typeof authorizationHeaderValue !== 'string') { + return null; + } + + return authorizationHeaderValue.split(/\s+/)[0].toLowerCase(); +} diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 77f1f9e45aea72..c634e2c80c2994 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -9,11 +9,12 @@ import { Authentication } from '.'; export const authenticationMock = { create: (): jest.Mocked => ({ login: jest.fn(), + logout: jest.fn(), + isProviderEnabled: jest.fn(), createAPIKey: jest.fn(), getCurrentUser: jest.fn(), invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), - logout: jest.fn(), getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 3727b1fc13dac9..aaf3fc357352ee 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -61,7 +61,7 @@ describe('setupAuthentication()', () => { lifespan: null, }, cookieName: 'my-sid-cookie', - authc: { providers: ['basic'] }, + authc: { providers: ['basic'], http: { enabled: true } }, }), true ); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 467afe00340259..189babbc6bfe67 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -169,6 +169,7 @@ export async function setupAuthentication({ login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), getSessionInfo: authenticator.getSessionInfo.bind(authenticator), + isProviderEnabled: authenticator.isProviderEnabled.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index a659786f4aeffe..0781608f8bc4c0 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; -import { ScopedClusterClient } from '../../../../../../src/core/server'; -import { Tokens } from '../tokens'; import { loggingServiceMock, httpServiceMock, @@ -17,34 +14,7 @@ export type MockAuthenticationProviderOptions = ReturnType< typeof mockAuthenticationProviderOptions >; -export type MockAuthenticationProviderOptionsWithJest = ReturnType< - typeof mockAuthenticationProviderOptionsWithJest ->; - -export function mockScopedClusterClient( - client: MockAuthenticationProviderOptions['client'], - requestMatcher: sinon.SinonMatcher = sinon.match.any -) { - const scopedClusterClient = sinon.createStubInstance(ScopedClusterClient); - client.asScoped.withArgs(requestMatcher).returns(scopedClusterClient); - return scopedClusterClient; -} - export function mockAuthenticationProviderOptions() { - const logger = loggingServiceMock.create().get(); - const basePath = httpServiceMock.createSetupContract().basePath; - basePath.get.mockReturnValue('/base-path'); - - return { - client: { callAsInternalUser: sinon.stub(), asScoped: sinon.stub(), close: sinon.stub() }, - logger, - basePath, - tokens: sinon.createStubInstance(Tokens), - }; -} - -// Will be renamed to mockAuthenticationProviderOptions as soon as we migrate all providers tests to Jest. -export function mockAuthenticationProviderOptionsWithJest() { const basePath = httpServiceMock.createSetupContract().basePath; basePath.get.mockReturnValue('/base-path'); diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index a40732768810d9..300e59d9ea3dac 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -84,6 +84,13 @@ export abstract class BaseAuthenticationProvider { */ abstract logout(request: KibanaRequest, state?: unknown): Promise; + /** + * Returns HTTP authentication scheme that provider uses within `Authorization` HTTP header that + * it attaches to all successfully authenticated requests to Elasticsearch or `null` in case + * provider doesn't attach any additional `Authorization` HTTP headers. + */ + abstract getHTTPAuthenticationScheme(): string | null; + /** * Queries Elasticsearch `_authenticate` endpoint to authenticate request and retrieve the user * information of authenticated user. diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index f9c665d6cea48c..b7bdff0531fc29 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -4,18 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; - -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { mockAuthenticationProviderOptions, mockScopedClusterClient } from './base.mock'; +import { mockAuthenticationProviderOptions } from './base.mock'; +import { IClusterClient, ScopeableRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { BasicAuthenticationProvider } from './basic'; function generateAuthorizationHeader(username: string, password: string) { return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; } +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('BasicAuthenticationProvider', () => { let provider: BasicAuthenticationProvider; let mockOptions: ReturnType; @@ -30,38 +43,39 @@ describe('BasicAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.login( - httpServerMock.createKibanaRequest(), - credentials + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect( + provider.login(httpServerMock.createKibanaRequest({ headers: {} }), credentials) + ).resolves.toEqual( + AuthenticationResult.succeeded(user, { + authHeaders: { authorization }, + state: { authorization }, + }) ); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toEqual({ authorization }); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); }); it('fails if user cannot be retrieved during login attempt', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); const authenticationError = new Error('Some error'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -69,142 +83,113 @@ describe('BasicAuthenticationProvider', () => { it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and // avoid triggering of redirect logic. - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), - null - ); - - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ path: '/s/foo/some-path # that needs to be encoded' }), - null - ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ + path: '/s/foo/some-path # that needs to be encoded', + }), + null + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + ) ); }); it('does not handle authentication if state exists, but authorization property is missing.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest(), - {} - ); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(httpServerMock.createKibanaRequest(), {}) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); - it('succeeds if only `authorization` header is available.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: generateAuthorizationHeader('user', 'password') }, - }); - const user = mockAuthenticatedUser(); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: request.headers })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request); + it('does not handle authentication via `authorization` header.', async () => { + const authorization = generateAuthorizationHeader('user', 'password'); + const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - // Session state and authHeaders aren't returned for header-based auth. - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe(authorization); }); - it('succeeds if only state is available.', async () => { - const request = httpServerMock.createKibanaRequest(); - const user = mockAuthenticatedUser(); + it('does not handle authentication via `authorization` header even if state contains valid credentials.', async () => { const authorization = generateAuthorizationHeader('user', 'password'); + const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, { authorization }); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe(authorization); }); - it('does not handle `authorization` header with unsupported schema even if state contains valid credentials.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer ***' }, - }); + it('succeeds if only state is available.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const user = mockAuthenticatedUser(); const authorization = generateAuthorizationHeader('user', 'password'); - const authenticationResult = await provider.authenticate(request, { authorization }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Bearer ***'); - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization } }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); }); it('fails if state contains invalid credentials.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = generateAuthorizationHeader('user', 'password'); const authenticationError = new Error('Forbidden'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); - - const authenticationResult = await provider.authenticate(request, { authorization }); - - expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.error).toBe(authenticationError); - }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - it('authenticates only via `authorization` header even if state is available.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: generateAuthorizationHeader('user', 'password') }, - }); - const user = mockAuthenticatedUser(); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: request.headers })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - const authorizationInState = generateAuthorizationHeader('user1', 'password2'); - const authenticationResult = await provider.authenticate(request, { - authorization: authorizationInState, - }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); + expect(request.headers).not.toHaveProperty('authorization'); }); }); describe('`logout` method', () => { it('always redirects to the login page.', async () => { - const request = httpServerMock.createKibanaRequest(); - const deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); - expect(deauthenticateResult.redirectURL).toBe('/base-path/login?msg=LOGGED_OUT'); + await expect(provider.logout(httpServerMock.createKibanaRequest())).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); }); it('passes query string parameters to the login page.', async () => { - const request = httpServerMock.createKibanaRequest({ - query: { next: '/app/ml', msg: 'SESSION_EXPIRED' }, - }); - const deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); - expect(deauthenticateResult.redirectURL).toBe( - '/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' + await expect( + provider.logout( + httpServerMock.createKibanaRequest({ query: { next: '/app/ml', msg: 'SESSION_EXPIRED' } }) + ) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED') ); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('basic'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/basic.ts b/x-pack/plugins/security/server/authentication/providers/basic.ts index a8e4e8705a7a8d..ad46aff8afa518 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.ts @@ -8,6 +8,7 @@ import { KibanaRequest } from '../../../../../../src/core/server'; import { canRedirectRequest } from '../can_redirect_request'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { BaseAuthenticationProvider } from './base'; /** @@ -75,29 +76,25 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // try header-based auth - const { - authenticationResult: headerAuthResult, - headerNotRecognized, - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return headerAuthResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - let authenticationResult = headerAuthResult; - if (authenticationResult.notHandled() && state) { - authenticationResult = await this.authenticateViaState(request, state); - } else if (authenticationResult.notHandled() && canRedirectRequest(request)) { - // If we couldn't handle authentication let's redirect user to the login page. - const nextURL = encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` - ); - authenticationResult = AuthenticationResult.redirectTo( - `${this.options.basePath.get(request)}/login?next=${nextURL}` + if (state) { + return await this.authenticateViaState(request, state); + } + + // If state isn't present let's redirect user to the login page. + if (canRedirectRequest(request)) { + this.logger.debug('Redirecting request to Login page.'); + const basePath = this.options.basePath.get(request); + return AuthenticationResult.redirectTo( + `${basePath}/login?next=${encodeURIComponent(`${basePath}${request.url.path}`)}` ); } - return authenticationResult; + return AuthenticationResult.notHandled(); } /** @@ -114,37 +111,11 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Validates whether request contains `Basic ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { authenticationResult: AuthenticationResult.notHandled() }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'basic') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { - authenticationResult: AuthenticationResult.notHandled(), - headerNotRecognized: true, - }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - return { authenticationResult: AuthenticationResult.succeeded(user) }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { authenticationResult: AuthenticationResult.failed(err) }; - } + public getHTTPAuthenticationScheme() { + return 'basic'; } /** diff --git a/x-pack/plugins/security/server/authentication/providers/http.test.ts b/x-pack/plugins/security/server/authentication/providers/http.test.ts new file mode 100644 index 00000000000000..65fbd7cd9f4adf --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/http.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; + +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { HTTPAuthenticationProvider } from './http'; + +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + +describe('HTTPAuthenticationProvider', () => { + let mockOptions: MockAuthenticationProviderOptions; + beforeEach(() => { + mockOptions = mockAuthenticationProviderOptions(); + }); + + it('throws if `schemes` are not specified', () => { + const providerOptions = mockAuthenticationProviderOptions(); + + expect(() => new HTTPAuthenticationProvider(providerOptions, undefined as any)).toThrowError( + 'Supported schemes should be specified' + ); + expect(() => new HTTPAuthenticationProvider(providerOptions, {} as any)).toThrowError( + 'Supported schemes should be specified' + ); + expect( + () => new HTTPAuthenticationProvider(providerOptions, { supportedSchemes: new Set() }) + ).toThrowError('Supported schemes should be specified'); + }); + + describe('`login` method', () => { + it('does not handle login', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect(provider.login()).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + }); + + describe('`authenticate` method', () => { + it('does not handle authentication for requests without `authorization` header.', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect(provider.authenticate(httpServerMock.createKibanaRequest())).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('does not handle authentication for requests with empty scheme in `authorization` header.', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { authorization: '' } }) + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('does not handle authentication via `authorization` header if scheme is not supported.', async () => { + for (const { schemes, header } of [ + { schemes: ['basic'], header: 'Bearer xxx' }, + { schemes: ['bearer'], header: 'Basic xxx' }, + { schemes: ['basic', 'apikey'], header: 'Bearer xxx' }, + { schemes: ['basic', 'bearer'], header: 'ApiKey xxx' }, + ]) { + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(schemes), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(request.headers.authorization).toBe(header); + } + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('succeeds if authentication via `authorization` header with supported scheme succeeds.', async () => { + const user = mockAuthenticatedUser(); + for (const { schemes, header } of [ + { schemes: ['basic'], header: 'Basic xxx' }, + { schemes: ['bearer'], header: 'Bearer xxx' }, + { schemes: ['basic', 'apikey'], header: 'ApiKey xxx' }, + { schemes: ['some-weird-scheme'], header: 'some-weird-scheme xxx' }, + { schemes: ['apikey', 'bearer'], header: 'Bearer xxx' }, + ]) { + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(schemes), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded({ ...user, authentication_provider: 'http' }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + } + }); + + it('fails if authentication via `authorization` header with supported scheme fails.', async () => { + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + for (const { schemes, header } of [ + { schemes: ['basic'], header: 'Basic xxx' }, + { schemes: ['bearer'], header: 'Bearer xxx' }, + { schemes: ['basic', 'apikey'], header: 'ApiKey xxx' }, + { schemes: ['some-weird-scheme'], header: 'some-weird-scheme xxx' }, + { schemes: ['apikey', 'bearer'], header: 'Bearer xxx' }, + ]) { + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(schemes), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + } + }); + }); + + describe('`logout` method', () => { + it('does not handle logout', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect(provider.logout()).resolves.toEqual(DeauthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + }); + + it('`getHTTPAuthenticationScheme` method', () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + expect(provider.getHTTPAuthenticationScheme()).toBeNull(); + }); +}); diff --git a/x-pack/plugins/security/server/authentication/providers/http.ts b/x-pack/plugins/security/server/authentication/providers/http.ts new file mode 100644 index 00000000000000..57163bf8145b8d --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/http.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; +import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; + +interface HTTPAuthenticationProviderOptions { + supportedSchemes: Set; +} + +/** + * Provider that supports request authentication via forwarding `Authorization` HTTP header to Elasticsearch. + */ +export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { + /** + * Type of the provider. + */ + static readonly type = 'http'; + + /** + * Set of the schemes (`Basic`, `Bearer` etc.) that provider expects to see within `Authorization` + * HTTP header while authenticating request. + */ + private readonly supportedSchemes: Set; + + constructor( + protected readonly options: Readonly, + httpOptions: Readonly + ) { + super(options); + + if ((httpOptions?.supportedSchemes?.size ?? 0) === 0) { + throw new Error('Supported schemes should be specified'); + } + this.supportedSchemes = httpOptions.supportedSchemes; + } + + /** + * NOT SUPPORTED. + */ + public async login() { + this.logger.debug('Login is not supported.'); + return AuthenticationResult.notHandled(); + } + + /** + * Performs request authentication using provided `Authorization` HTTP headers. + * @param request Request instance. + */ + public async authenticate(request: KibanaRequest) { + this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + + const authenticationScheme = getHTTPAuthenticationScheme(request); + if (authenticationScheme == null) { + this.logger.debug('Authorization header is not presented.'); + return AuthenticationResult.notHandled(); + } + + if (!this.supportedSchemes.has(authenticationScheme)) { + this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`); + return AuthenticationResult.notHandled(); + } + + try { + const user = await this.getUser(request); + this.logger.debug( + `Request to ${request.url.path} has been authenticated via authorization header with "${authenticationScheme}" scheme.` + ); + return AuthenticationResult.succeeded(user); + } catch (err) { + this.logger.debug( + `Failed to authenticate request to ${request.url.path} via authorization header with "${authenticationScheme}" scheme: ${err.message}` + ); + return AuthenticationResult.failed(err); + } + } + + /** + * NOT SUPPORTED. + */ + public async logout() { + this.logger.debug('Logout is not supported.'); + return DeauthenticationResult.notHandled(); + } + + /** + * Returns `null` since provider doesn't attach any additional `Authorization` HTTP headers to + * successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return null; + } +} diff --git a/x-pack/plugins/security/server/authentication/providers/index.ts b/x-pack/plugins/security/server/authentication/providers/index.ts index 1ec6dfb67a81d5..cd8f5a70c64e3d 100644 --- a/x-pack/plugins/security/server/authentication/providers/index.ts +++ b/x-pack/plugins/security/server/authentication/providers/index.ts @@ -15,3 +15,4 @@ export { SAMLAuthenticationProvider, isSAMLRequestQuery, SAMLLoginStep } from '. export { TokenAuthenticationProvider } from './token'; export { OIDCAuthenticationProvider, OIDCAuthenticationFlow } from './oidc'; export { PKIAuthenticationProvider } from './pki'; +export { HTTPAuthenticationProvider } from './http'; diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index e4b4df3feeae21..51fb961482e83d 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -6,18 +6,31 @@ import Boom from 'boom'; import { errors } from 'elasticsearch'; -import sinon from 'sinon'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { KerberosAuthenticationProvider } from './kerberos'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server/elasticsearch'; + +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} describe('KerberosAuthenticationProvider', () => { let provider: KerberosAuthenticationProvider; @@ -28,104 +41,128 @@ describe('KerberosAuthenticationProvider', () => { }); describe('`authenticate` method', () => { - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header with non-negotiate scheme.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header with non-negotiate scheme even if state contains a valid token.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.client.asScoped); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('does not handle requests that can be authenticated without `Negotiate` header.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ - headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, - }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves({}); + const request = httpServerMock.createKibanaRequest({ headers: {} }); - const authenticationResult = await provider.authenticate(request, null); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({}); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('does not handle requests if backend does not support Kerberos.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ - headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, - }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - const authenticationResult = await provider.authenticate(request, null); - expect(authenticationResult.notHandled()).toBe(true); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('fails if state is present, but backend does not support Kerberos.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'token', refreshToken: 'refresh-token' }; - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.tokens.refresh.mockResolvedValue(null); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); }); it('fails with `Negotiate` challenge if backend supports Kerberos.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ - headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError( + new (errors.AuthenticationException as any)('Unauthorized', { + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects( - ElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) - ) - ); - - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toEqual({ 'WWW-Authenticate': 'Negotiate' }); + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason, { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); + + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('fails if request authentication is failed with non-401 error.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(new errors.ServiceUnavailable()); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const failureReason = new errors.ServiceUnavailable(); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('status', 503); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('gets a token pair in exchange to SPNEGO one and stores it in the state.', async () => { @@ -134,34 +171,33 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: 'some-token', + refresh_token: 'some-refresh-token', + }); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .resolves({ access_token: 'some-token', refresh_token: 'some-refresh-token' }); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer some-token' }, + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + } + ) + ); - const authenticationResult = await provider.authenticate(request); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer some-token' }, + }); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, + }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer some-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('requests auth response header if token pair is complemented with Kerberos response token.', async () => { @@ -170,38 +206,35 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - mockOptions.client.callAsInternalUser.withArgs('shield.getAccessToken').resolves({ + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'some-token', refresh_token: 'some-refresh-token', kerberos_authentication_response_token: 'response-token', }); - const authenticationResult = await provider.authenticate(request); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer some-token' }, + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' }, + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + } + ) ); - expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer some-token' }); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate response-token', + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer some-token' }, }); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); + + expect(request.headers.authorization).toBe('negotiate spnego'); }); it('fails with `Negotiate response-token` if cannot complete context with a response token.', async () => { @@ -214,24 +247,19 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate response-token' } } }, }) ); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized(), { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' }, + }) ); - expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual(Boom.unauthorized()); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate response-token', + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); + + expect(request.headers.authorization).toBe('negotiate spnego'); }); it('fails with `Negotiate` if cannot create context using provided SPNEGO token.', async () => { @@ -244,24 +272,19 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized(), { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) ); - expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual(Boom.unauthorized()); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate', + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); + + expect(request.headers.authorization).toBe('negotiate spnego'); }); it('fails if could not retrieve an access token in exchange to SPNEGO one.', async () => { @@ -270,22 +293,17 @@ describe('KerberosAuthenticationProvider', () => { }); const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, + }); + expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve user using the new access token.', async () => { @@ -294,51 +312,52 @@ describe('KerberosAuthenticationProvider', () => { }); const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .resolves({ access_token: 'some-token', refresh_token: 'some-refresh-token' }); - - const authenticationResult = await provider.authenticate(request); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: 'some-token', + refresh_token: 'some-refresh-token', + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer some-token' }, + }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, + }); + expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { authHeaders: { authorization } } + ) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toBeUndefined(); }); it('succeeds with valid session even if requiring a token refresh', async () => { @@ -346,149 +365,94 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())); - - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'newfoo', refreshToken: 'newbar' }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer newfoo' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'newfoo', + refreshToken: 'newbar', + }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer newfoo' }, + state: { accessToken: 'newfoo', refreshToken: 'newbar' }, + } + ) + ); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer newfoo' }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' }); expect(request.headers).not.toHaveProperty('authorization'); }); it('fails if token from the state is rejected because of unknown reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; const failureReason = new errors.InternalServerError('Token is not valid!'); - const scopedClusterClient = mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); - scopedClusterClient.callAsCurrentUser.withArgs('shield.authenticate').rejects(failureReason); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Bearer ${tokenPair.accessToken}` }, + }); + + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - sinon.assert.neverCalledWith(scopedClusterClient.callAsCurrentUser, 'shield.getAccessToken'); }); it('fails with `Negotiate` challenge if both access and refresh tokens from the state are expired and backend supports Kerberos.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'expired-token', refreshToken: 'some-valid-refresh-token' }; - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects( - ElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) - ) - ); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toEqual({ 'WWW-Authenticate': 'Negotiate' }); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-valid-token' }, - }); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-valid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request); - - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - }); - - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-invalid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError( + new (errors.AuthenticationException as any)('Unauthorized', { + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, + }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); + mockOptions.tokens.refresh.mockResolvedValue(null); - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - }); - const tokenPair = { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - }; + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason, { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-invalid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); }); }); @@ -496,13 +460,13 @@ describe('KerberosAuthenticationProvider', () => { it('returns `notHandled` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.tokens.invalidate); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); it('fails if `tokens.invalidate` fails', async () => { @@ -510,15 +474,14 @@ describe('KerberosAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const failureReason = new Error('failed to delete token'); - mockOptions.tokens.invalidate.withArgs(tokenPair).rejects(failureReason); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); it('redirects to `/logged_out` page if tokens are invalidated successfully.', async () => { @@ -528,15 +491,18 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'some-valid-refresh-token', }; - mockOptions.tokens.invalidate.withArgs(tokenPair).resolves(); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockResolvedValue(undefined); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index b8e3b7bc237901..b6474a5e1d471d 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -12,27 +12,15 @@ import { } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { BaseAuthenticationProvider } from './base'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; +import { BaseAuthenticationProvider } from './base'; /** * The state supported by the provider. */ type ProviderState = TokenPair; -/** - * Parses request's `Authorization` HTTP header if present and extracts authentication scheme. - * @param request Request instance to extract authentication scheme for. - */ -function getRequestAuthenticationScheme(request: KibanaRequest) { - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - return ''; - } - - return authorization.split(/\s+/)[0].toLowerCase(); -} - /** * Name of the `WWW-Authenticate` we parse out of Elasticsearch responses or/and return to the * client to initiate or continue negotiation. @@ -56,24 +44,15 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - const authenticationScheme = getRequestAuthenticationScheme(request); - if ( - authenticationScheme && - authenticationScheme !== 'negotiate' && - authenticationScheme !== 'bearer' - ) { + const authenticationScheme = getHTTPAuthenticationScheme(request); + if (authenticationScheme && authenticationScheme !== 'negotiate') { this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`); return AuthenticationResult.notHandled(); } - let authenticationResult = AuthenticationResult.notHandled(); - if (authenticationScheme) { - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - authenticationResult = - authenticationScheme === 'bearer' - ? await this.authenticateWithBearerScheme(request) - : await this.authenticateWithNegotiateScheme(request); - } + let authenticationResult = authenticationScheme + ? await this.authenticateWithNegotiateScheme(request) + : AuthenticationResult.notHandled(); if (state && authenticationResult.notHandled()) { authenticationResult = await this.authenticateViaState(request, state); @@ -115,6 +94,14 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.redirectTo(`${this.options.basePath.serverBasePath}/logged_out`); } + /** + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return 'bearer'; + } + /** * Tries to authenticate request with `Negotiate ***` Authorization header by passing it to the Elasticsearch backend to * get an access token in exchange. @@ -201,26 +188,6 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { } } - /** - * Tries to authenticate request with `Bearer ***` Authorization header by passing it to the Elasticsearch backend. - * @param request Request instance. - */ - private async authenticateWithBearerScheme(request: KibanaRequest) { - this.logger.debug('Trying to authenticate request using "Bearer" authentication scheme.'); - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated using "Bearer" authentication scheme.'); - return AuthenticationResult.succeeded(user); - } catch (err) { - this.logger.debug( - `Failed to authenticate request using "Bearer" authentication scheme: ${err.message}` - ); - return AuthenticationResult.failed(err); - } - } - /** * Tries to extract access token from state and adds it to the request before it's * forwarded to Elasticsearch backend. diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index dae37749558591..51a25825bf985a 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -4,20 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; import Boom from 'boom'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { KibanaRequest } from '../../../../../../src/core/server'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + KibanaRequest, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { OIDCAuthenticationProvider, OIDCAuthenticationFlow, ProviderLoginAttempt } from './oidc'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('OIDCAuthenticationProvider', () => { let provider: OIDCAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -44,7 +58,7 @@ describe('OIDCAuthenticationProvider', () => { it('redirects third party initiated login attempts to the OpenId Connect Provider.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/security/oidc/callback' }); - mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ state: 'statevalue', nonce: 'noncevalue', redirect: @@ -56,30 +70,27 @@ describe('OIDCAuthenticationProvider', () => { '&login_hint=loginhint', }); - const authenticationResult = await provider.login(request, { - flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, - iss: 'theissuer', - loginHint: 'loginhint', - }); + await expect( + provider.login(request, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: 'theissuer', + loginHint: 'loginhint', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + + '&login_hint=loginhint', + { state: { state: 'statevalue', nonce: 'noncevalue', nextURL: '/base-path/' } } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { iss: 'theissuer', login_hint: 'loginhint' }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + - '&login_hint=loginhint' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/', - }); }); function defineAuthenticationFlowTests( @@ -92,18 +103,24 @@ describe('OIDCAuthenticationProvider', () => { it('gets token and redirects user to requested URL if OIDC authentication response is valid.', async () => { const { request, attempt, expectedRedirectURI } = getMocks(); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcAuthenticate') - .resolves({ access_token: 'some-token', refresh_token: 'some-refresh-token' }); - - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/some-path', + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: 'some-token', + refresh_token: 'some-refresh-token', }); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + await expect( + provider.login(request, attempt, { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/some-path', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/some-path', { + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + }) + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.oidcAuthenticate', { body: { @@ -114,58 +131,52 @@ describe('OIDCAuthenticationProvider', () => { }, } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/some-path'); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails if authentication response is presented but session state does not contain the state parameter.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, { - nextURL: '/base-path/some-path', - }); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest( - 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + await expect( + provider.login(request, attempt, { nextURL: '/base-path/some-path' }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if authentication response is presented but session state does not contain redirect URL.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - }); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest( - 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + await expect( + provider.login(request, attempt, { state: 'statevalue', nonce: 'noncevalue' }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if session state is not presented.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, {}); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + await expect(provider.login(request, attempt, {})).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) + ) + ); - expect(authenticationResult.failed()).toBe(true); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if authentication response is not valid.', async () => { @@ -174,18 +185,17 @@ describe('OIDCAuthenticationProvider', () => { const failureReason = new Error( 'Failed to exchange code for Id Token using the Token Endpoint.' ); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcAuthenticate') - .returns(Promise.reject(failureReason)); - - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/some-path', - }); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + await expect( + provider.login(request, attempt, { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/some-path', + }) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.oidcAuthenticate', { body: { @@ -196,9 +206,6 @@ describe('OIDCAuthenticationProvider', () => { }, } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); } @@ -234,16 +241,15 @@ describe('OIDCAuthenticationProvider', () => { describe('`authenticate` method', () => { it('does not handle AJAX request that can not be authenticated.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); it('redirects non-AJAX request that can not be authenticated to the OpenId Connect Provider.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ state: 'statevalue', nonce: 'noncevalue', redirect: @@ -254,84 +260,99 @@ describe('OIDCAuthenticationProvider', () => { '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', + { + state: { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/s/foo/some-path', + }, + } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/s/foo/some-path', - }); }); it('fails if OpenID Connect authentication request preparation fails.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/some-path' }); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcPrepare') - .returns(Promise.reject(failureReason)); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'oidc' }, + { authHeaders: { authorization } } + ) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toBeUndefined(); }); - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, }); - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(request, { + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + }) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('fails if token from the state is rejected because of unknown reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-invalid-token', refreshToken: 'some-invalid-refresh-token', @@ -339,15 +360,17 @@ describe('OIDCAuthenticationProvider', () => { const authorization = `Bearer ${tokenPair.accessToken}`; const failureReason = new Error('Token is not valid!'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => { @@ -355,67 +378,80 @@ describe('OIDCAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer new-access-token') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer new-access-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }); - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'oidc' }, + { + authHeaders: { authorization: 'Bearer new-access-token' }, + state: { accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }, + } + ) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: 'Bearer new-access-token', - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toEqual({ - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); }); it('fails if token from the state is expired and refresh attempt failed too.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'expired-token', refreshToken: 'invalid-refresh-token' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const refreshFailureReason = { statusCode: 500, message: 'Something is wrong with refresh token.', }; - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).rejects(refreshFailureReason); + mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(refreshFailureReason as any) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(refreshFailureReason); }); it('redirects to OpenID Connect Provider for non-AJAX requests if refresh token is expired or already refreshed.', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path', headers: {} }); const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ state: 'statevalue', nonce: 'noncevalue', redirect: @@ -426,115 +462,71 @@ describe('OIDCAuthenticationProvider', () => { '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', + { + state: { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/s/foo/some-path', + }, + } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { - body: { realm: `oidc1` }, + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); + + expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + headers: { authorization }, }); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/s/foo/some-path', + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { + body: { realm: `oidc1` }, }); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); - - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-valid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request); + mockOptions.tokens.refresh.mockResolvedValue(null); - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); - const authenticationResult = await provider.authenticate(request); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); - - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-valid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + expectAuthenticateCall(mockOptions.client, { + headers: { 'kbn-xsrf': 'xsrf', authorization }, }); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(request.headers).not.toHaveProperty('authorization'); }); }); @@ -542,16 +534,19 @@ describe('OIDCAuthenticationProvider', () => { it('returns `notHandled` if state is not presented or does not include access token.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request, {}); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, undefined as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - deauthenticateResult = await provider.logout(request, {}); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, {})).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - deauthenticateResult = await provider.logout(request, { nonce: 'x' }); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, { nonce: 'x' })).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if OpenID Connect logout call fails.', async () => { @@ -560,22 +555,16 @@ describe('OIDCAuthenticationProvider', () => { const refreshToken = 'x-oidc-refresh-token'; const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcLogout') - .returns(Promise.reject(failureReason)); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to /logged_out if `redirect` field in OpenID Connect logout response is null.', async () => { @@ -583,22 +572,16 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcLogout') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects user to the OpenID Connect Provider if RP initiated SLO is supported.', async () => { @@ -606,18 +589,22 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcLogout') - .resolves({ redirect: 'http://fake-idp/logout&id_token_hint=thehint' }); - - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, + mockOptions.client.callAsInternalUser.mockResolvedValue({ + redirect: 'http://fake-idp/logout&id_token_hint=thehint', }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/logout&id_token_hint=thehint'); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/logout&id_token_hint=thehint') + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { + body: { token: accessToken, refresh_token: refreshToken }, + }); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index f13a2ec05231a2..c6b504e722adf4 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -10,6 +10,7 @@ import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; import { DeauthenticationResult } from '../deauthentication_result'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; import { AuthenticationProviderOptions, @@ -130,16 +131,13 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - let { - authenticationResult, - headerNotRecognized, // eslint-disable-line prefer-const - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return authenticationResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - if (state && authenticationResult.notHandled()) { + let authenticationResult = AuthenticationResult.notHandled(); + if (state) { authenticationResult = await this.authenticateViaState(request, state); if ( authenticationResult.failed() && @@ -276,46 +274,6 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { } } - /** - * Validates whether request contains `Bearer ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. - */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { - authenticationResult: AuthenticationResult.notHandled(), - }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'bearer') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { - authenticationResult: AuthenticationResult.notHandled(), - headerNotRecognized: true, - }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - return { - authenticationResult: AuthenticationResult.succeeded(user), - }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { - authenticationResult: AuthenticationResult.failed(err), - }; - } - } - /** * Tries to extract an elasticsearch access token from state and adds it to the request before it's * forwarded to Elasticsearch backend. @@ -444,4 +402,12 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.failed(err); } } + + /** + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return 'bearer'; + } } diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index a2dda88c4680c3..efc286c6c895f7 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -7,23 +7,23 @@ jest.mock('net'); jest.mock('tls'); +import { Socket } from 'net'; import { PeerCertificate, TLSSocket } from 'tls'; +import Boom from 'boom'; import { errors } from 'elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptionsWithJest, - mockAuthenticationProviderOptionsWithJest, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { PKIAuthenticationProvider } from './pki'; import { ElasticsearchErrorHelpers, - ScopedClusterClient, -} from '../../../../../../src/core/server/elasticsearch'; -import { Socket } from 'net'; -import { getErrorStatusCode } from '../../errors'; + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { PKIAuthenticationProvider } from './pki'; interface MockPeerCertificate extends Partial { issuerCertificate: MockPeerCertificate; @@ -62,32 +62,59 @@ function getMockSocket({ return socket; } +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('PKIAuthenticationProvider', () => { let provider: PKIAuthenticationProvider; - let mockOptions: MockAuthenticationProviderOptionsWithJest; + let mockOptions: MockAuthenticationProviderOptions; beforeEach(() => { - mockOptions = mockAuthenticationProviderOptionsWithJest(); + mockOptions = mockAuthenticationProviderOptions(); provider = new PKIAuthenticationProvider(mockOptions); }); afterEach(() => jest.clearAllMocks()); describe('`authenticate` method', () => { - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); const state = { accessToken: 'some-valid-token', peerCertificateFingerprint256: '2A:7A:C2:DD', }; - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('does not handle requests without certificate.', async () => { @@ -95,9 +122,10 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ authorized: true }), }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -107,9 +135,10 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }), }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -121,12 +150,10 @@ describe('PKIAuthenticationProvider', () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toMatchInlineSnapshot( - `[Error: Peer certificate is not available]` + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(new Error('Peer certificate is not available')) ); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -134,10 +161,9 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -151,10 +177,9 @@ describe('PKIAuthenticationProvider', () => { }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -174,12 +199,18 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -191,22 +222,11 @@ describe('PKIAuthenticationProvider', () => { }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('gets an access token in exchange to a self-signed certificate and stores it in the state.', async () => { @@ -221,34 +241,29 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('invalidates existing token and gets a new one if fingerprints do not match.', async () => { @@ -263,12 +278,18 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -286,14 +307,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('gets a new access token even if existing token is expired.', async () => { @@ -312,12 +325,18 @@ describe('PKIAuthenticationProvider', () => { .mockRejectedValueOnce(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())) // In response to a call with a new token. .mockResolvedValueOnce(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -330,14 +349,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('fails with 401 if existing token is expired, but certificate is not present.', async () => { @@ -348,18 +359,15 @@ describe('PKIAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve an access token in exchange to peer certificate chain.', async () => { @@ -373,7 +381,9 @@ describe('PKIAuthenticationProvider', () => { const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -381,9 +391,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve user using the new access token.', async () => { @@ -398,35 +405,30 @@ describe('PKIAuthenticationProvider', () => { const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const request = httpServerMock.createKibanaRequest({ + headers: {}, socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), @@ -435,110 +437,42 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { authHeaders: { authorization: `Bearer ${state.accessToken}` } } + ) ); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: `Bearer ${state.accessToken}`, - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if token from the state is rejected because of unknown reason.', async () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const request = httpServerMock.createKibanaRequest({ + headers: {}, socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), }), }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(new errors.ServiceUnavailable()); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); - - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('status', 503); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-valid-token' }, - }); - - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); - - const authenticationResult = await provider.authenticate(request); - - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - }); - - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + const failureReason = new errors.ServiceUnavailable(); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); - - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), - }), - }); - - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser - // In response to call with a token from header. - .mockRejectedValueOnce(failureReason) - // In response to a call with a token from session (not expected to be called). - .mockResolvedValueOnce(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); }); }); @@ -546,11 +480,11 @@ describe('PKIAuthenticationProvider', () => { it('returns `notHandled` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -562,13 +496,12 @@ describe('PKIAuthenticationProvider', () => { const failureReason = new Error('failed to delete token'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, state); + await expect(provider.logout(request, state)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to `/logged_out` page if access token is invalidated successfully.', async () => { @@ -577,13 +510,16 @@ describe('PKIAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, state); + await expect(provider.logout(request, state)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 6d5aa9f01f2ea3..854f92a50fa9d5 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -9,8 +9,9 @@ import { DetailedPeerCertificate } from 'tls'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { BaseAuthenticationProvider } from './base'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens } from '../tokens'; +import { BaseAuthenticationProvider } from './base'; /** * The state supported by the provider. @@ -27,19 +28,6 @@ interface ProviderState { peerCertificateFingerprint256: string; } -/** - * Parses request's `Authorization` HTTP header if present and extracts authentication scheme. - * @param request Request instance to extract authentication scheme for. - */ -function getRequestAuthenticationScheme(request: KibanaRequest) { - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - return ''; - } - - return authorization.split(/\s+/)[0].toLowerCase(); -} - /** * Provider that supports PKI request authentication. */ @@ -57,19 +45,13 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - const authenticationScheme = getRequestAuthenticationScheme(request); - if (authenticationScheme && authenticationScheme !== 'bearer') { - this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`); + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); return AuthenticationResult.notHandled(); } let authenticationResult = AuthenticationResult.notHandled(); - if (authenticationScheme) { - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - authenticationResult = await this.authenticateWithBearerScheme(request); - } - - if (state && authenticationResult.notHandled()) { + if (state) { authenticationResult = await this.authenticateViaState(request, state); // If access token expired or doesn't match to the certificate fingerprint we should try to get @@ -120,23 +102,11 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Tries to authenticate request with `Bearer ***` Authorization header by passing it to the Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateWithBearerScheme(request: KibanaRequest) { - this.logger.debug('Trying to authenticate request using "Bearer" authentication scheme.'); - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated using "Bearer" authentication scheme.'); - return AuthenticationResult.succeeded(user); - } catch (err) { - this.logger.debug( - `Failed to authenticate request using "Bearer" authentication scheme: ${err.message}` - ); - return AuthenticationResult.failed(err); - } + public getHTTPAuthenticationScheme() { + return 'bearer'; } /** diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index c4fdf0b25061b7..d97a6c0838b86a 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -5,19 +5,33 @@ */ import Boom from 'boom'; -import sinon from 'sinon'; import { ByteSizeValue } from '@kbn/config-schema'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { SAMLAuthenticationProvider, SAMLLoginStep } from './saml'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('SAMLAuthenticationProvider', () => { let provider: SAMLAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -63,294 +77,317 @@ describe('SAMLAuthenticationProvider', () => { it('gets token and redirects user to requested URL if SAML Response is valid.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'user', access_token: 'some-token', refresh_token: 'some-refresh-token', }); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path#some-app' } + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path#some-app' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/test-base-path/some-path#some-app', { + state: { + username: 'user', + accessToken: 'some-token', + refreshToken: 'some-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/test-base-path/some-path#some-app'); - expect(authenticationResult.state).toEqual({ - username: 'user', - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails if SAML Response payload is presented but state does not contain SAML Request token.', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - {} + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + {} + ) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest('SAML response state does not have corresponding request id.') + ) ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('SAML response state does not have corresponding request id.') - ); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('redirects to the default location if state contains empty redirect URL.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'user-initiated-login-token', refresh_token: 'user-initiated-login-refresh-token', }); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '' } + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + accessToken: 'user-initiated-login-token', + refreshToken: 'user-initiated-login-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); - expect(authenticationResult.state).toEqual({ - accessToken: 'user-initiated-login-token', - refreshToken: 'user-initiated-login-refresh-token', - }); }); it('redirects to the default location if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'idp-initiated-login-token', refresh_token: 'idp-initiated-login-refresh-token', }); - const authenticationResult = await provider.login(request, { - step: SAMLLoginStep.SAMLResponseReceived, - samlResponse: 'saml-response-xml', - }); + await expect( + provider.login(request, { + step: SAMLLoginStep.SAMLResponseReceived, + samlResponse: 'saml-response-xml', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + accessToken: 'idp-initiated-login-token', + refreshToken: 'idp-initiated-login-refresh-token', + }, + }) + ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); - expect(authenticationResult.state).toEqual({ - accessToken: 'idp-initiated-login-token', - refreshToken: 'idp-initiated-login-refresh-token', - }); }); it('fails if SAML Response is rejected.', async () => { const request = httpServerMock.createKibanaRequest(); const failureReason = new Error('SAML response is stale!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlAuthenticate') - .rejects(failureReason); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path' } - ); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); describe('IdP initiated login with existing session', () => { it('fails if new SAML Response is rejected.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const authorization = 'Bearer some-valid-token'; const user = mockAuthenticatedUser(); - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const failureReason = new Error('SAML response is invalid!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlAuthenticate') - .rejects(failureReason); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { + username: 'user', + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', { - username: 'user', - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, } ); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } - ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails if fails to invalidate existing access/refresh tokens.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'existing-valid-token', refreshToken: 'existing-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const user = mockAuthenticatedUser(); - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', }); const failureReason = new Error('Failed to invalidate token!'); - mockOptions.tokens.invalidate.rejects(failureReason); + mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state - ); + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } ); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, { + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to the home page if new SAML Response is for the same user.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'existing-valid-token', refreshToken: 'existing-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const user = { username: 'user' }; - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', }); - mockOptions.tokens.invalidate.resolves(); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } ); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, { + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); }); it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'existing-valid-token', refreshToken: 'existing-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const existingUser = { username: 'user' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(existingUser); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'new-user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', }); - mockOptions.tokens.invalidate.resolves(); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } ); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, { + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/overwritten_session'); }); }); @@ -358,170 +395,171 @@ describe('SAMLAuthenticationProvider', () => { it('fails if state is not available', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await provider.login(request, { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('State does not include URL path to redirect to.') + await expect( + provider.login(request, { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest('State does not include URL path to redirect to.') + ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('does not handle AJAX requests.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } - ); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('redirects non-AJAX requests to the IdP remembering combined redirect URL.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#some-fragment', + }, + } + ) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path#some-fragment', - }); }); it('prepends redirect URL fragment with `#` if it does not have one.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '../some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '../some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#../some-fragment', + }, + } + ) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); expect(mockOptions.logger.warn).toHaveBeenCalledTimes(1); expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Redirect URL fragment does not start with `#`.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path#../some-fragment', - }); }); it('redirects non-AJAX requests to the IdP remembering only redirect URL path if fragment is too large.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment'.repeat(10), - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment'.repeat(10), + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path', + }, + } + ) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); expect(mockOptions.logger.warn).toHaveBeenCalledTimes(1); expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL size should not exceed 100b but it was 165b. Only URL path is captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path', - }); }); it('fails if SAML request preparation fails.', async () => { const request = httpServerMock.createKibanaRequest(); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').rejects(failureReason); - - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } - ); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); }); }); }); @@ -530,44 +568,57 @@ describe('SAMLAuthenticationProvider', () => { it('does not handle AJAX request that can not be authenticated.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - username: 'user', - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, }); - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(request, { + username: 'user', + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + }) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('redirects non-AJAX request that can not be authenticated to the "capture fragment" page.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.authenticate(request); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/mock-server-basepath/api/security/saml/capture-url-fragment' + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/api/security/saml/capture-url-fragment', + { state: { redirectURL: '/base-path/s/foo/some-path' } } + ) ); - expect(authenticationResult.state).toEqual({ redirectURL: '/base-path/s/foo/some-path' }); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('redirects non-AJAX request that can not be authenticated to the IdP if request path is too large.', async () => { @@ -575,14 +626,19 @@ describe('SAMLAuthenticationProvider', () => { path: `/s/foo/${'some-path'.repeat(10)}`, }); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { state: { requestId: 'some-request-id', redirectURL: '' } } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); @@ -590,12 +646,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL path size should not exceed 100b but it was 107b. URL is not captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ requestId: 'some-request-id', redirectURL: '' }); }); it('fails if SAML request preparation fails.', async () => { @@ -604,21 +654,20 @@ describe('SAMLAuthenticationProvider', () => { }); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'some-valid-token', @@ -626,40 +675,43 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'saml' }, + { authHeaders: { authorization } } + ) + ); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if token from the state is rejected because of unknown reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const failureReason = { statusCode: 500, message: 'Token is not valid!' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(failureReason as any) + ); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => { @@ -671,65 +723,80 @@ describe('SAMLAuthenticationProvider', () => { refreshToken: 'valid-refresh-token', }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer new-access-token') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer new-access-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }); - mockOptions.tokens.refresh - .withArgs(state.refreshToken) - .resolves({ accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'saml' }, + { + authHeaders: { authorization: 'Bearer new-access-token' }, + state: { + username: 'user', + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }, + } + ) + ); - const authenticationResult = await provider.authenticate(request, state); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: 'Bearer new-access-token', - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toEqual({ - username: 'user', - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); }); it('fails if token from the state is expired and refresh attempt failed with unknown reason too.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'expired-token', refreshToken: 'invalid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const refreshFailureReason = { statusCode: 500, message: 'Something is wrong with refresh token.', }; - mockOptions.tokens.refresh.withArgs(state.refreshToken).rejects(refreshFailureReason); + mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(refreshFailureReason as any) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(refreshFailureReason); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { @@ -739,80 +806,100 @@ describe('SAMLAuthenticationProvider', () => { accessToken: 'expired-token', refreshToken: 'expired-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.tokens.refresh.mockResolvedValue(null); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); - mockOptions.tokens.refresh.withArgs(state.refreshToken).resolves(null); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { + headers: { 'kbn-xsrf': 'xsrf', authorization }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); }); it('re-capture URL for non-AJAX requests if refresh token is expired.', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path', headers: {} }); const state = { username: 'user', accessToken: 'expired-token', refreshToken: 'expired-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.withArgs(state.refreshToken).resolves(null); + mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/api/security/saml/capture-url-fragment', + { state: { redirectURL: '/base-path/s/foo/some-path' } } + ) + ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/mock-server-basepath/api/security/saml/capture-url-fragment' - ); - expect(authenticationResult.state).toEqual({ redirectURL: '/base-path/s/foo/some-path' }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('initiates SAML handshake for non-AJAX requests if refresh token is expired and request path is too large.', async () => { const request = httpServerMock.createKibanaRequest({ path: `/s/foo/${'some-path'.repeat(10)}`, + headers: {}, }); const state = { username: 'user', accessToken: 'expired-token', refreshToken: 'expired-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.withArgs(state.refreshToken).resolves(null); + mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { state: { requestId: 'some-request-id', redirectURL: '' } } + ) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlPrepare', { + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); @@ -820,71 +907,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL path size should not exceed 100b but it was 107b. URL is not captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ requestId: 'some-request-id', redirectURL: '' }); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-valid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request); - - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); - - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-valid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); }); @@ -892,16 +914,15 @@ describe('SAMLAuthenticationProvider', () => { it('returns `notHandled` if state is not presented or does not include access token.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); - - deauthenticateResult = await provider.logout(request, {} as any); - expect(deauthenticateResult.notHandled()).toBe(true); - - deauthenticateResult = await provider.logout(request, { somethingElse: 'x' } as any); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); + await expect(provider.logout(request, {} as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); + await expect(provider.logout(request, { somethingElse: 'x' } as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if SAML logout call fails.', async () => { @@ -910,42 +931,32 @@ describe('SAMLAuthenticationProvider', () => { const refreshToken = 'x-saml-refresh-token'; const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.withArgs('shield.samlLogout').rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.failed(failureReason)); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails if SAML invalidate call fails.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request); - - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects to /logged_out if `redirect` field in SAML logout response is null.', async () => { @@ -953,23 +964,16 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML logout response is not defined.', async () => { @@ -977,23 +981,16 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: undefined }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML logout if query string is not empty, but does not include SAMLRequest.', async () => { @@ -1003,87 +1000,65 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML invalidate call even if access token is presented.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: null }); - - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken: 'x-saml-token', - refreshToken: 'x-saml-refresh-token', - }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } - ); + await expect( + provider.logout(request, { + username: 'user', + accessToken: 'x-saml-token', + refreshToken: 'x-saml-refresh-token', + }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is null.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request); - - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: undefined }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); - const authenticationResult = await provider.logout(request); - - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects user to the IdP if SLO is supported by IdP in case of SP initiated logout.', async () => { @@ -1091,37 +1066,41 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H' }); - - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, + mockOptions.client.callAsInternalUser.mockResolvedValue({ + redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/SLO?SAMLRequest=7zlH37H'); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); }); it('redirects user to the IdP if SLO is supported by IdP in case of IdP initiated logout.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H' }); - - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken: 'x-saml-token', - refreshToken: 'x-saml-refresh-token', + mockOptions.client.callAsInternalUser.mockResolvedValue({ + redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/SLO?SAMLRequest=7zlH37H'); + await expect( + provider.logout(request, { + username: 'user', + accessToken: 'x-saml-token', + refreshToken: 'x-saml-refresh-token', + }) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index a817159fcd445c..1ac59d66a22359 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -9,9 +9,10 @@ import { ByteSizeValue } from '@kbn/config-schema'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; import { canRedirectRequest } from '../can_redirect_request'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; +import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; /** * The state supported by the provider (for the SAML handshake or established session). @@ -180,17 +181,13 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - let { - authenticationResult, - // eslint-disable-next-line prefer-const - headerNotRecognized, - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return authenticationResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - if (state && authenticationResult.notHandled()) { + let authenticationResult = AuthenticationResult.notHandled(); + if (state) { authenticationResult = await this.authenticateViaState(request, state); if ( authenticationResult.failed() && @@ -243,37 +240,11 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Validates whether request contains `Bearer ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { authenticationResult: AuthenticationResult.notHandled() }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'bearer') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { - authenticationResult: AuthenticationResult.notHandled(), - headerNotRecognized: true, - }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - return { authenticationResult: AuthenticationResult.succeeded(user) }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { authenticationResult: AuthenticationResult.failed(err) }; - } + public getHTTPAuthenticationScheme() { + return 'bearer'; } /** 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 0a55219e25d910..e81d14e8bf9f35 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -6,18 +6,32 @@ import Boom from 'boom'; import { errors } from 'elasticsearch'; -import sinon from 'sinon'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { TokenAuthenticationProvider } from './token'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('TokenAuthenticationProvider', () => { let provider: TokenAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -28,29 +42,35 @@ describe('TokenAuthenticationProvider', () => { describe('`login` method', () => { it('succeeds with valid login attempt, creates session and authHeaders', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const user = mockAuthenticatedUser(); const credentials = { username: 'user', password: 'password' }; const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken', { - body: { grant_type: 'password', ...credentials }, - }) - .resolves({ access_token: tokenPair.accessToken, refresh_token: tokenPair.refreshToken }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: tokenPair.accessToken, + refresh_token: tokenPair.refreshToken, + }); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { authHeaders: { authorization }, state: tokenPair } + ) + ); - const authenticationResult = await provider.login(request, credentials); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toEqual(tokenPair); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: 'password', ...credentials }, + }); }); it('fails if token cannot be generated during login attempt', async () => { @@ -58,109 +78,125 @@ describe('TokenAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authenticationError = new Error('Invalid credentials'); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken', { - body: { grant_type: 'password', ...credentials }, - }) - .rejects(authenticationError); + mockOptions.client.callAsInternalUser.mockRejectedValue(authenticationError); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - sinon.assert.notCalled(mockOptions.client.asScoped); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: 'password', ...credentials }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); it('fails if user cannot be retrieved during login attempt', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const credentials = { username: 'user', password: 'password' }; const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken', { - body: { grant_type: 'password', ...credentials }, - }) - .resolves({ access_token: tokenPair.accessToken, refresh_token: tokenPair.refreshToken }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: tokenPair.accessToken, + refresh_token: tokenPair.refreshToken, + }); const authenticationError = new Error('Some error'); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: 'password', ...credentials }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); describe('`authenticate` method', () => { - it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { - // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and - // avoid triggering of redirect logic. - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), - null - ); - - expect(authenticationResult.notHandled()).toBe(true); - }); + it('does not handle authentication via `authorization` header.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); - it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ path: '/s/foo/some-path # that needs to be encoded' }), - null + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' - ); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); - it('succeeds if only `authorization` header is available and returns neither state nor authHeaders.', async () => { - const authorization = 'Bearer foo'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const user = mockAuthenticatedUser(); + it('does not handle authentication via `authorization` header even if state contains valid credentials.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + await expect( + provider.authenticate(request, { accessToken: 'foo', refreshToken: 'bar' }) + ).resolves.toEqual(AuthenticationResult.notHandled()); - const authenticationResult = await provider.authenticate(request); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); + it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { + // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and + // avoid triggering of redirect logic. + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + }); + + it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ + path: '/s/foo/some-path # that needs to be encoded', + }), + null + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + ) + ); }); it('succeeds if only state is available.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const user = mockAuthenticatedUser(); const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { authHeaders: { authorization } } + ) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toEqual({ authorization }); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -169,162 +205,115 @@ describe('TokenAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); - - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'newfoo', refreshToken: 'newbar' }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer newfoo' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'newfoo', + refreshToken: 'newbar', + }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { + authHeaders: { authorization: 'Bearer newfoo' }, + state: { accessToken: 'newfoo', refreshToken: 'newbar' }, + } + ) + ); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer newfoo' }); expect(request.headers).not.toHaveProperty('authorization'); }); - it('does not handle `authorization` header with unsupported schema even if state contains valid credentials.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic ***' }, - }); + it('fails if authentication with token from state fails with unknown error.', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const user = mockAuthenticatedUser(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Basic ***'); - expect(authenticationResult.notHandled()).toBe(true); - }); - - it('authenticates only via `authorization` header even if state is available.', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const authorization = `Bearer foo-from-header`; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const user = mockAuthenticatedUser(); - - // GetUser will be called with request's `authorization` header. - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(request.headers.authorization).toEqual('Bearer foo-from-header'); - }); - - it('fails if authentication with token from header fails with unknown error', async () => { - const authorization = `Bearer foo`; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const authenticationError = new errors.InternalServerError('something went wrong'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); - - const authenticationResult = await provider.authenticate(request); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); - }); - - it('fails if authentication with token from state fails with unknown error.', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const request = httpServerMock.createKibanaRequest(); - - const authenticationError = new errors.InternalServerError('something went wrong'); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); it('fails if token refresh is rejected with unknown error', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const refreshError = new errors.InternalServerError('failed to refresh token'); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).rejects(refreshError); + mockOptions.tokens.refresh.mockRejectedValue(refreshError); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(refreshError) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(refreshError); }); it('redirects non-AJAX requests to /login and clears session if token cannot be refreshed', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/some-path' }); + const request = httpServerMock.createKibanaRequest({ path: '/some-path', headers: {} }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.tokens.refresh.mockResolvedValue(null); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/login?next=%2Fbase-path%2Fsome-path', { + state: null, + }) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fsome-path' - ); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toEqual(null); - expect(authenticationResult.error).toBeUndefined(); }); it('does not redirect AJAX requests if token token cannot be refreshed', async () => { @@ -333,61 +322,66 @@ describe('TokenAuthenticationProvider', () => { path: '/some-path', }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + mockOptions.tokens.refresh.mockResolvedValue(null); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expectAuthenticateCall(mockOptions.client, { + headers: { 'kbn-xsrf': 'xsrf', authorization }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if new access token is rejected after successful refresh', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); - - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'newfoo', refreshToken: 'newbar' }); - const authenticationError = new errors.AuthenticationException('Some error'); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer newfoo' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - const authenticationResult = await provider.authenticate(request, tokenPair); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'newfoo', + refreshToken: 'newbar', + }); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -395,13 +389,15 @@ describe('TokenAuthenticationProvider', () => { it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.redirected()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - sinon.assert.notCalled(mockOptions.tokens.invalidate); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); it('fails if `tokens.invalidate` fails', async () => { @@ -409,45 +405,46 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const failureReason = new Error('failed to delete token'); - mockOptions.tokens.invalidate.withArgs(tokenPair).rejects(failureReason); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); it('redirects to /login if tokens are invalidated successfully', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockOptions.tokens.invalidate.withArgs(tokenPair).resolves(); + mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, tokenPair); - - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/login?msg=LOGGED_OUT'); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); it('redirects to /login with optional search parameters if tokens are invalidated successfully', async () => { const request = httpServerMock.createKibanaRequest({ query: { yep: 'nope' } }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockOptions.tokens.invalidate.withArgs(tokenPair).resolves(); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockResolvedValue(undefined); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?yep=nope') + ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/login?yep=nope'); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 03fd003e2cbde2..fffac254ed30ac 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -8,9 +8,10 @@ import Boom from 'boom'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { BaseAuthenticationProvider } from './base'; import { canRedirectRequest } from '../can_redirect_request'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; +import { BaseAuthenticationProvider } from './base'; /** * Describes the parameters that are required by the provider to process the initial login request. @@ -34,12 +35,6 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { */ static readonly type = 'token'; - /** - * Performs initial login request using username and password. - * @param request Request instance. - * @param loginAttempt Login attempt description. - * @param [state] Optional state object associated with the provider. - */ /** * Performs initial login request using username and password. * @param request Request instance. @@ -87,18 +82,13 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // if there isn't a payload, try header-based token auth - const { - authenticationResult: headerAuthResult, - headerNotRecognized, - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return headerAuthResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - let authenticationResult = headerAuthResult; - // if we still can't attempt auth, try authenticating via state (session token) - if (authenticationResult.notHandled() && state) { + let authenticationResult = AuthenticationResult.notHandled(); + if (state) { authenticationResult = await this.authenticateViaState(request, state); if ( authenticationResult.failed() && @@ -111,6 +101,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { // finally, if authentication still can not be handled for this // request/state combination, redirect to the login page if appropriate if (authenticationResult.notHandled() && canRedirectRequest(request)) { + this.logger.debug('Redirecting request to Login page.'); authenticationResult = AuthenticationResult.redirectTo(this.getLoginPageURL(request)); } @@ -144,37 +135,11 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Validates whether request contains `Bearer ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { authenticationResult: AuthenticationResult.notHandled() }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'bearer') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { authenticationResult: AuthenticationResult.notHandled(), headerNotRecognized: true }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - - // We intentionally do not store anything in session state because token - // header auth can only be used on a request by request basis. - return { authenticationResult: AuthenticationResult.succeeded(user) }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { authenticationResult: AuthenticationResult.failed(err) }; - } + public getHTTPAuthenticationScheme() { + return 'bearer'; } /** diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index f7374eedb5520d..64c695670fa19f 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,57 +13,78 @@ 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, - "session": Object { - "idleTimeout": null, - "lifespan": null, - }, - } - `); + Object { + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "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, - "session": Object { - "idleTimeout": null, - "lifespan": null, - }, - } - `); + Object { + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "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, - "session": Object { - "idleTimeout": null, - "lifespan": null, - }, - } - `); + Object { + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "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', () => { @@ -101,15 +122,22 @@ describe('config schema', () => { authc: { providers: ['oidc'], oidc: { realm: 'realm-1' } }, }).authc ).toMatchInlineSnapshot(` - Object { - "oidc": Object { - "realm": "realm-1", - }, - "providers": Array [ - "oidc", - ], - } - `); + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "oidc": Object { + "realm": "realm-1", + }, + "providers": Array [ + "oidc", + ], + } + `); }); it(`returns a validation error when authc.providers is "['oidc', 'basic']" and realm is unspecified`, async () => { @@ -126,16 +154,23 @@ describe('config schema', () => { authc: { providers: ['oidc', 'basic'], oidc: { realm: 'realm-1' } }, }).authc ).toMatchInlineSnapshot(` - Object { - "oidc": Object { - "realm": "realm-1", - }, - "providers": Array [ - "oidc", - "basic", - ], - } - `); + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "oidc": Object { + "realm": "realm-1", + }, + "providers": Array [ + "oidc", + "basic", + ], + } + `); }); it(`realm is not allowed when authc.providers is "['basic']"`, async () => { @@ -164,18 +199,25 @@ describe('config schema', () => { authc: { providers: ['saml'], saml: { realm: 'realm-1' } }, }).authc ).toMatchInlineSnapshot(` - Object { - "providers": Array [ - "saml", - ], - "saml": Object { - "maxRedirectURLSize": ByteSizeValue { - "valueInBytes": 2048, - }, - "realm": "realm-1", - }, - } - `); + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "providers": Array [ + "saml", + ], + "saml": Object { + "maxRedirectURLSize": ByteSizeValue { + "valueInBytes": 2048, + }, + "realm": "realm-1", + }, + } + `); }); it('`realm` is not allowed if saml provider is not enabled', async () => { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index db8c48f314d7ce..8663a6e61c2030 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -49,6 +49,11 @@ export const ConfigSchema = schema.object( maxRedirectURLSize: schema.byteSize({ defaultValue: '2kb' }), }) ), + http: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + autoSchemesEnabled: schema.boolean({ defaultValue: true }), + schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }), + }), }), }, // This option should be removed as soon as we entirely migrate config from legacy Security plugin. diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index c0e86b289fe545..e1167af0be7f05 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -32,6 +32,17 @@ export const config: PluginConfigDescriptor> = { deprecations: ({ rename, unused }) => [ rename('sessionTimeout', 'session.idleTimeout'), unused('authorization.legacyFallback.enabled'), + (settings, fromPath, log) => { + const hasProvider = (provider: string) => + settings?.xpack?.security?.authc?.providers?.includes(provider) ?? false; + + if (hasProvider('basic') && hasProvider('token')) { + log( + 'Enabling both `basic` and `token` authentication providers in `xpack.security.authc.providers` is deprecated. Login page will only use `token` provider.' + ); + } + return settings; + }, ], }; export const plugin: PluginInitializer< diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 56aad4ece3e958..6f5c79e873e868 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -28,6 +28,7 @@ describe('Security Plugin', () => { authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, + http: { enabled: true, autoSchemesEnabled: true, schemes: ['apikey'] }, }, }) ); @@ -77,6 +78,7 @@ describe('Security Plugin', () => { "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], + "isProviderEnabled": [Function], "login": [Function], "logout": [Function], }, diff --git a/x-pack/plugins/security/server/routes/authentication/basic.test.ts b/x-pack/plugins/security/server/routes/authentication/basic.test.ts index be17b3e29f8548..cc1c94d799be61 100644 --- a/x-pack/plugins/security/server/routes/authentication/basic.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/basic.test.ts @@ -33,7 +33,9 @@ describe('Basic authentication routes', () => { let mockContext: RequestHandlerContext; beforeEach(() => { router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + authc.isProviderEnabled.mockImplementation(provider => provider === 'basic'); mockContext = ({ licensing: { @@ -166,6 +168,22 @@ describe('Basic authentication routes', () => { value: { username: 'user', password: 'password' }, }); }); + + it('prefers `token` authentication provider if it is enabled', async () => { + authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); + authc.isProviderEnabled.mockImplementation( + provider => provider === 'token' || provider === 'basic' + ); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'token', + value: { username: 'user', password: 'password' }, + }); + }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/basic.ts b/x-pack/plugins/security/server/routes/authentication/basic.ts index 453dc1c4ea3b55..db36e45fc07e89 100644 --- a/x-pack/plugins/security/server/routes/authentication/basic.ts +++ b/x-pack/plugins/security/server/routes/authentication/basic.ts @@ -25,16 +25,13 @@ export function defineBasicRoutes({ router, authc, config }: RouteDefinitionPara options: { authRequired: false }, }, createLicensedRouteHandler(async (context, request, response) => { - const { username, password } = request.body; + // We should prefer `token` over `basic` if possible. + const loginAttempt = authc.isProviderEnabled('token') + ? { provider: 'token', value: request.body } + : { provider: 'basic', value: request.body }; try { - // We should prefer `token` over `basic` if possible. - const providerToLoginWith = config.authc.providers.includes('token') ? 'token' : 'basic'; - const authenticationResult = await authc.login(request, { - provider: providerToLoginWith, - value: { username, password }, - }); - + const authenticationResult = await authc.login(request, loginAttempt); if (!authenticationResult.succeeded()) { return response.unauthorized({ body: authenticationResult.error }); } diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 6035025564cbfe..a774edfb4ab2c0 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -27,18 +27,15 @@ export function defineAuthenticationRoutes(params: RouteDefinitionParams) { defineSessionRoutes(params); defineCommonRoutes(params); - if ( - params.config.authc.providers.includes('basic') || - params.config.authc.providers.includes('token') - ) { + if (params.authc.isProviderEnabled('basic') || params.authc.isProviderEnabled('token')) { defineBasicRoutes(params); } - if (params.config.authc.providers.includes('saml')) { + if (params.authc.isProviderEnabled('saml')) { defineSAMLRoutes(params); } - if (params.config.authc.providers.includes('oidc')) { + if (params.authc.isProviderEnabled('oidc')) { defineOIDCRoutes(params); } } diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index d819dd38dddb1b..ef7e48388ff660 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const config = getService('config'); const kibanaServerConfig = config.get('servers.kibana'); @@ -25,7 +25,7 @@ export default function({ getService }: FtrProviderContext) { return response; }; const getSessionInfo = async () => - supertest + supertestWithoutAuth .get('/internal/security/session') .set('kbn-xsrf', 'xxx') .set('kbn-system-request', 'true') @@ -33,7 +33,7 @@ export default function({ getService }: FtrProviderContext) { .send() .expect(200); const extendSession = async () => - supertest + supertestWithoutAuth .post('/internal/security/session') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) @@ -42,7 +42,7 @@ export default function({ getService }: FtrProviderContext) { .then(saveCookie); beforeEach(async () => { - await supertest + await supertestWithoutAuth .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) From 5046a3a68da1cfa59ca95797be3dff09e4046a00 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 28 Feb 2020 10:13:41 +0000 Subject: [PATCH 29/34] clean up snapshot (#58724) --- .../__snapshots__/zeek_details.test.tsx.snap | 3971 ----------------- .../body/renderers/zeek/zeek_details.test.tsx | 1 - 2 files changed, 3972 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index c883983f8cf010..0a60c8facff9c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -498,3974 +498,3 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` timelineId="test" /> `; - -exports[`ZeekDetails rendering it returns zeek.files if the data does contain zeek.files data 1`] = ` -.c3, -.c3::before, -.c3::after { - -webkit-transition: background 150ms ease, color 150ms ease; - transition: background 150ms ease, color 150ms ease; -} - -.c3 { - border-radius: 2px; - padding: 0 4px 0 8px; - position: relative; - z-index: 0 !important; -} - -.c3::before { - background-image: linear-gradient( 135deg, #535966 25%, transparent 25% ), linear-gradient( -135deg, #535966 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #535966 75% ), linear-gradient( -135deg, transparent 75%, #535966 75% ); - background-position: 0 0,1px 0,1px -1px,0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; -} - -.c3:hover, -.c3:hover .euiBadge, -.c3:hover .euiBadge__text { - cursor: move; - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; -} - -.event-column-view:hover .c3, -tr:hover .c3 { - background-color: #343741; -} - -.event-column-view:hover .c3::before, -tr:hover .c3::before { - background-image: linear-gradient( 135deg, #98a2b3 25%, transparent 25% ), linear-gradient( -135deg, #98a2b3 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #98a2b3 75% ), linear-gradient( -135deg, transparent 75%, #98a2b3 75% ); -} - -.c3:hover, -.c3:focus, -.event-column-view:hover .c3:hover, -.event-column-view:focus .c3:focus, -tr:hover .c3:hover, -tr:hover .c3:focus { - background-color: #1ba9f5; -} - -.c3:hover, -.c3:focus, -.event-column-view:hover .c3:hover, -.event-column-view:focus .c3:focus, -tr:hover .c3:hover, -tr:hover .c3:focus, -.c3:hover a, -.c3:focus a, -.event-column-view:hover .c3:hover a, -.event-column-view:focus .c3:focus a, -tr:hover .c3:hover a, -tr:hover .c3:focus a, -.c3:hover a:hover, -.c3:focus a:hover, -.event-column-view:hover .c3:hover a:hover, -.event-column-view:focus .c3:focus a:hover, -tr:hover .c3:hover a:hover, -tr:hover .c3:focus a:hover { - color: #1d1e24; -} - -.c3:hover::before, -.c3:focus::before, -.event-column-view:hover .c3:hover::before, -.event-column-view:focus .c3:focus::before, -tr:hover .c3:hover::before, -tr:hover .c3:focus::before { - background-image: linear-gradient( 135deg, #1d1e24 25%, transparent 25% ), linear-gradient( -135deg, #1d1e24 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #1d1e24 75% ), linear-gradient( -135deg, transparent 75%, #1d1e24 75% ); -} - -.c2 { - display: inline-block; - max-width: 100%; -} - -.c2 [data-rbd-placeholder-context-id] { - display: none !important; -} - -.c4 > span.euiToolTipAnchor { - display: block; -} - -.c8 { - margin: 0 2px; -} - -.c7 { - margin-top: 3px; -} - -.c6 { - margin-right: 10px; -} - -.c1 { - margin-left: 3px; -} - -.c5 { - margin-left: 6px; -} - -.c0 { - margin: 5px 0; -} - - - - - - - - - - - - - - - -
-
- - -
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - Cu0n232QMyvNtzb75j - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - files - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - sha1: fa5195a... - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - md5: f7653f1... - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - -
-
- -
- - - - - - - - -
-
- -
-
-
-
-
-
-
-
- -
- - - - -
- -
- - -
- - -
- - -
- - -
- - - - -
- - -
- - -
- - - -
- - -
- -
- - -
- - -
- - - -
- - -
- -
- -
-
- - - -
- - - - -
- -
-
-
-
- -
- - -
-
- -
-
-
-
- -
-
- -
- - -
- - -
- -
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx index db51ade6df4c5a..b45e4c41762bc7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -113,7 +113,6 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper).toMatchSnapshot(); expect(wrapper.text()).toEqual('Cu0n232QMyvNtzb75jfilessha1: fa5195a...md5: f7653f1...'); }); From 967bef7b3877ab67e0c213fd5fa67a66ad14f5d1 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 28 Feb 2020 13:59:43 +0200 Subject: [PATCH 30/34] [SIEM][CASE] Init Configure Case Page (#58121) * [SIEM][CASE] Init configure cases * [SIEM][CASE] Translate header title * [SIEM][CASE] Add back link * [SIEM][CASE] Add default options to header page * [SIEM][CASE] Create configure cases page redirections and links * [SIEM][CASE] Add configure cases button * [SIEM][CASE] Change translation variable * [SIEM][CASE] Create wrappers * [SIEM][CASE]Create section wrapper * [SIEM][CASE] Switch to new wrapper * [SIEM][CASE] Add translations * [SIEM][CASE] Add connectors dropdown component * [SIEM][CASE] Add connectors component * [SIEM][CASE] Show connectors * [SIEM][CASE] Create add new connector button * [SIEM][CASE] Change values * [SIEM][CASE] Use state for connectors dropdown * [SIEM][CASE] Remove unnecessary attribute * [SIEM][CASE] Remove timeline in configuration page * [SIEM][CASE] Remove text from gear button * [SIEM][CASE] make show timeline more generic so we can re-use if need it Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../siem/public/components/link_to/index.ts | 2 + .../public/components/link_to/link_to.tsx | 11 +++- .../components/link_to/redirect_to_case.tsx | 4 ++ .../plugins/siem/public/pages/case/case.tsx | 27 +++++---- .../components/case_header_page/index.tsx | 22 ++++++++ .../case_header_page/translations.ts | 16 ++++++ .../pages/case/components/case_view/index.tsx | 13 +---- .../components/configure_cases/connectors.tsx | 52 +++++++++++++++++ .../connectors_dropdown/index.tsx | 56 +++++++++++++++++++ .../configure_cases/translations.ts | 37 ++++++++++++ .../pages/case/components/wrappers/index.tsx | 22 ++++++++ .../public/pages/case/configure_cases.tsx | 54 ++++++++++++++++++ .../siem/public/pages/case/create_case.tsx | 10 +--- .../plugins/siem/public/pages/case/index.tsx | 5 ++ .../siem/public/pages/case/translations.ts | 20 ++++--- .../plugins/siem/public/pages/home/index.tsx | 5 +- .../utils/timeline/use_show_timeline.tsx | 31 ++++++++++ 17 files changed, 345 insertions(+), 42 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx create mode 100644 x-pack/legacy/plugins/siem/public/utils/timeline/use_show_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index c93b415e017bb7..a1c1f78e398e33 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -17,6 +17,8 @@ export { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl, + getConfigureCasesUrl, RedirectToCasePage, RedirectToCreatePage, + RedirectToConfigureCasesPage, } from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index c08b429dc4625e..08e4d1a3494e06 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,7 +20,11 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; -import { RedirectToCasePage, RedirectToCreatePage } from './redirect_to_case'; +import { + RedirectToCasePage, + RedirectToCreatePage, + RedirectToConfigureCasesPage, +} from './redirect_to_case'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { @@ -43,6 +47,11 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToCreatePage} path={`${match.url}/:pageName(${SiemPageName.case})/create`} /> + ; +export const RedirectToConfigureCasesPage = () => ( + +); const baseCaseUrl = `#/link-to/${SiemPageName.case}`; export const getCaseUrl = () => baseCaseUrl; export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; +export const getConfigureCasesUrl = () => `${baseCaseUrl}/configure`; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 1206ec950deed1..15a6d076f10095 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -6,30 +6,29 @@ import React from 'react'; -import { EuiButton, EuiFlexGroup } from '@elastic/eui'; -import { HeaderPage } from '../../components/header_page'; +import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { CaseHeaderPage } from './components/case_header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; -import { getCreateCaseUrl } from '../../components/link_to'; - -const badgeOptions = { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, -}; +import { getCreateCaseUrl, getConfigureCasesUrl } from '../../components/link_to'; export const CasesPage = React.memo(() => ( <> - + - - {i18n.CREATE_TITLE} - + + + {i18n.CREATE_TITLE} + + + + + - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx new file mode 100644 index 00000000000000..ae2664ca6e839d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { HeaderPage, HeaderPageProps } from '../../../../components/header_page'; +import * as i18n from './translations'; + +const CaseHeaderPageComponent: React.FC = props => ; + +CaseHeaderPageComponent.defaultProps = { + badgeOptions: { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, + }, +}; + +export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts new file mode 100644 index 00000000000000..9fcad926c03b84 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.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 { i18n } from '@kbn/i18n'; + +export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.caseView.pageBadgeLabel', { + defaultMessage: 'Beta', +}); + +export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.caseView.pageBadgeTooltip', { + defaultMessage: + 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 5cd71c5855d34f..df3e30a698b56e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -34,6 +34,7 @@ import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; +import { WhitePageWrapper } from '../wrappers'; interface Props { caseId: string; @@ -52,14 +53,6 @@ const MyWrapper = styled(WrapperPage)` padding-bottom: 0; `; -const BackgroundWrapper = styled.div` - ${({ theme }) => css` - background-color: ${theme.eui.euiColorEmptyShade}; - border-top: ${theme.eui.euiBorderThin}; - height: 100%; - `} -`; - export interface CaseProps { caseId: string; initialData: Case; @@ -279,7 +272,7 @@ export const CaseComponent = React.memo(({ caseId, initialData, isLoa - + @@ -305,7 +298,7 @@ export const CaseComponent = React.memo(({ caseId, initialData, isLoa - + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx new file mode 100644 index 00000000000000..561464e44c7032 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx @@ -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 React from 'react'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; + +import styled from 'styled-components'; + +import { ConnectorsDropdown } from './connectors_dropdown'; +import * as i18n from './translations'; + +const EuiFormRowExtended = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiFormRow__label { + width: 100%; + } + } +`; + +const ConnectorsComponent: React.FC = () => { + const dropDownLabel = ( + + {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} + + {i18n.ADD_NEW_CONNECTOR} + + + ); + + return ( + {i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}} + description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} + > + + + + + ); +}; + +export const Connectors = React.memo(ConnectorsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx new file mode 100644 index 00000000000000..c00baa04d78a0b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback } from 'react'; +import { EuiSuperSelect, EuiIcon, EuiSuperSelectOption } from '@elastic/eui'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; + +const ICON_SIZE = 'm'; + +const EuiIconExtended = styled(EuiIcon)` + margin-right: 13px; +`; + +const connectors: Array> = [ + { + value: 'no-connector', + inputDisplay: ( + <> + + {i18n.NO_CONNECTOR} + + ), + 'data-test-subj': 'no-connector', + }, + { + value: 'servicenow-connector', + inputDisplay: ( + <> + + {'My ServiceNow connector'} + + ), + 'data-test-subj': 'servicenow-connector', + }, +]; + +const ConnectorsDropdownComponent: React.FC = () => { + const [selectedConnector, selectConnector] = useState(connectors[0].value); + const onChange = useCallback(connector => selectConnector(connector), [selectedConnector]); + + return ( + + ); +}; + +export const ConnectorsDropdown = React.memo(ConnectorsDropdownComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts new file mode 100644 index 00000000000000..54d256b143f603 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.siem.case.configureCases.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to third-party incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.siem.case.configureCases.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect SIEM cases to a third-party incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.siem.case.configureCases.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const NO_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.noConnector', { + defaultMessage: 'No connector selected', +}); + +export const ADD_NEW_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.addNewConnector', { + defaultMessage: 'Add new connector option', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx new file mode 100644 index 00000000000000..772d78f948b798 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled, { css } from 'styled-components'; + +export const WhitePageWrapper = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border-top: ${theme.eui.euiBorderThin}; + height: 100%; + min-height: 100vh; + `} +`; + +export const SectionWrapper = styled.div` + box-sizing: content-box; + margin: 0 auto; + max-width: 1175px; +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx new file mode 100644 index 00000000000000..018f9dc9ade52a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx @@ -0,0 +1,54 @@ +/* + * 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 styled, { css } from 'styled-components'; + +import { WrapperPage } from '../../components/wrapper_page'; +import { CaseHeaderPage } from './components/case_header_page'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { getCaseUrl } from '../../components/link_to'; +import { WhitePageWrapper, SectionWrapper } from './components/wrappers'; +import { Connectors } from './components/configure_cases/connectors'; +import * as i18n from './translations'; + +const backOptions = { + href: getCaseUrl(), + text: i18n.BACK_TO_ALL, +}; + +const wrapperPageStyle: Record = { + paddingLeft: '0', + paddingRight: '0', + paddingBottom: '0', +}; + +export const FormWrapper = styled.div` + ${({ theme }) => css` + padding-top: ${theme.eui.paddingSizes.l}; + padding-bottom: ${theme.eui.paddingSizes.l}; + `} +`; + +const ConfigureCasesPageComponent: React.FC = () => ( + <> + + + + + + + + + + + + + + +); + +export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index 9bc356517cc686..2c7525264f71be 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { WrapperPage } from '../../components/wrapper_page'; import { Create } from './components/create'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { HeaderPage } from '../../components/header_page'; +import { CaseHeaderPage } from './components/case_header_page'; import * as i18n from './translations'; import { getCaseUrl } from '../../components/link_to'; @@ -17,15 +17,11 @@ const backOptions = { href: getCaseUrl(), text: i18n.BACK_TO_ALL, }; -const badgeOptions = { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, -}; + export const CreateCasePage = React.memo(() => ( <> - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index 9bd91b1c6d62dd..1bde9de1535b52 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -11,10 +11,12 @@ import { SiemPageName } from '../home/types'; import { CaseDetailsPage } from './case_details'; import { CasesPage } from './case'; import { CreateCasePage } from './create_case'; +import { ConfigureCasesPage } from './configure_cases'; const casesPagePath = `/:pageName(${SiemPageName.case})`; const caseDetailsPagePath = `${casesPagePath}/:detailName`; const createCasePagePath = `${casesPagePath}/create`; +const configureCasesPagePath = `${casesPagePath}/configure`; const CaseContainerComponent: React.FC = () => ( @@ -24,6 +26,9 @@ const CaseContainerComponent: React.FC = () => ( + + + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 4e878ba58411e4..265af0bde547f2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -57,15 +57,6 @@ export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', defaultMessage: 'Last updated', }); -export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.caseView.pageBadgeLabel', { - defaultMessage: 'Beta', -}); - -export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.caseView.pageBadgeTooltip', { - defaultMessage: - 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', -}); - export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { defaultMessage: 'Case Workflow Management within the Elastic SIEM', }); @@ -102,3 +93,14 @@ export const NO_TAGS = i18n.translate('xpack.siem.case.caseView.noTags', { export const TITLE_REQUIRED = i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { defaultMessage: 'A title is required.', }); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.siem.case.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', { + defaultMessage: 'Configure cases', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 806f6c7937077c..5d20993144af96 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -21,6 +21,7 @@ import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning' import { UseUrlState } from '../../components/url_state'; import { useWithSource } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; import { NotFoundPage } from '../404'; import { DetectionEngineContainer } from '../detection_engine'; import { HostsContainer } from '../hosts'; @@ -74,6 +75,8 @@ export const HomePageComponent = () => { const { browserFields, indexPattern, contentAvailable } = useWithSource(); + const [showTimeline] = useShowTimeline(); + return ( @@ -81,7 +84,7 @@ export const HomePageComponent = () => {
- {contentAvailable && ( + {contentAvailable && showTimeline && ( <> { + const currentLocation = useLocation(); + const [showTimeline, setShowTimeline] = useState( + !hideTimelineForRoutes.includes(currentLocation.pathname) + ); + + useEffect(() => { + if (hideTimelineForRoutes.includes(currentLocation.pathname)) { + if (showTimeline) { + setShowTimeline(false); + } + } else if (!showTimeline) { + setShowTimeline(true); + } + }, [currentLocation.pathname]); + + return [showTimeline]; +}; From 8f9004b661eb48ff841872b89fe076af3481ec3a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 28 Feb 2020 14:27:57 +0100 Subject: [PATCH 31/34] [ML] Transforms: NP ui/imports migration (#58469) - Migrates ui/* imports to NP. - Uses direct React mounting instead of Angular. - Service Cleanup. --- .../public/__mocks__/shared_imports.ts | 7 + .../plugins/transform/public/app/app.tsx | 49 ++++--- .../transform/public/app/app_dependencies.tsx | 43 +++--- .../public/app/common/navigation.tsx | 4 +- .../public/app/components/section_error.tsx | 30 +--- .../toast_notification_text.test.tsx | 25 +++- .../components/toast_notification_text.tsx | 9 +- .../transform/public/app/constants/index.ts | 2 +- .../transform/public/app/hooks/index.ts | 1 + .../transform/public/app/hooks/use_api.ts | 10 +- .../public/app/hooks/use_delete_transform.tsx | 7 +- .../transform/public/app/hooks/use_request.ts | 16 +++ .../public/app/hooks/use_start_transform.ts | 3 +- .../public/app/hooks/use_stop_transform.ts | 3 +- .../components/authorization_provider.tsx | 14 +- .../public/app/lib/kibana/kibana_context.tsx | 1 - .../public/app/lib/kibana/kibana_provider.tsx | 10 +- .../clone_transform_section.tsx | 16 ++- .../source_index_preview.test.tsx | 2 - .../step_create/step_create_form.test.tsx | 2 - .../step_create/step_create_form.tsx | 15 +- .../step_define/pivot_preview.test.tsx | 2 - .../step_define/step_define_form.test.tsx | 76 +++++++--- .../step_define/step_define_form.tsx | 78 +++++----- .../step_define/step_define_summary.test.tsx | 2 - .../step_details/step_details_form.tsx | 10 +- .../create_transform_section.tsx | 7 +- .../create_transform_button.test.tsx | 2 - .../transform_list/action_clone.tsx | 2 +- .../transform_list/action_delete.test.tsx | 1 - .../transform_list/action_start.test.tsx | 1 - .../transform_list/action_stop.test.tsx | 1 - .../transform_list/actions.test.tsx | 2 - .../transform_list/columns.test.tsx | 2 - .../transform_list.test.mocks.ts | 15 -- .../transform_list/transform_list.test.tsx | 3 - .../transform_list/use_refresh_interval.ts | 62 +------- .../transform_management_section.test.tsx | 5 - .../transform_management_section.tsx | 6 +- .../documentation/documentation_links.ts | 20 --- .../app/services/documentation/index.ts | 7 - .../public/app/services/http/http.ts | 18 --- .../public/app/services/http/index.ts | 6 - .../public/app/services/http/use_request.ts | 22 --- .../public/app/services/http_service.ts | 81 ++++++----- .../app/services/navigation/breadcrumb.ts | 22 ++- .../legacy/plugins/transform/public/plugin.ts | 136 +++++++----------- .../transform/public/shared_imports.ts | 2 +- .../legacy/plugins/transform/public/shim.ts | 106 ++++++-------- 49 files changed, 422 insertions(+), 544 deletions(-) create mode 100644 x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts delete mode 100644 x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts delete mode 100644 x-pack/legacy/plugins/transform/public/app/services/documentation/documentation_links.ts delete mode 100644 x-pack/legacy/plugins/transform/public/app/services/documentation/index.ts delete mode 100644 x-pack/legacy/plugins/transform/public/app/services/http/http.ts delete mode 100644 x-pack/legacy/plugins/transform/public/app/services/http/index.ts delete mode 100644 x-pack/legacy/plugins/transform/public/app/services/http/use_request.ts diff --git a/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts index b55a4cd5c7bd6b..aa130b5030fc76 100644 --- a/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('ui/new_platform'); + export function XJsonMode() {} export function setDependencyCache() {} +export const useRequest = () => ({ + isLoading: false, + error: null, + data: undefined, +}); export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; export const SORT_DIRECTION = { ASC: 'asc' }; diff --git a/x-pack/legacy/plugins/transform/public/app/app.tsx b/x-pack/legacy/plugins/transform/public/app/app.tsx index 0f21afbcccca86..efbaabe447efa5 100644 --- a/x-pack/legacy/plugins/transform/public/app/app.tsx +++ b/x-pack/legacy/plugins/transform/public/app/app.tsx @@ -5,8 +5,8 @@ */ import React, { useContext, FC } from 'react'; -import { render } from 'react-dom'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter, Redirect, Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -22,8 +22,7 @@ import { TransformManagementSection } from './sections/transform_management'; export const App: FC = () => { const { apiError } = useContext(AuthorizationContext); - - if (apiError) { + if (apiError !== null) { return ( { return (
- - - - - - + + + + + + + +
); }; -export const renderReact = (elem: Element, appDependencies: AppDependencies) => { +export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => { const Providers = getAppProviders(appDependencies); render( , - elem + element ); + + return () => { + unmountComponentAtNode(element); + }; }; diff --git a/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx b/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx index 282d1380b396b0..21ffbf5911a216 100644 --- a/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx @@ -7,9 +7,6 @@ import React, { createContext, useContext, ReactNode } from 'react'; import { HashRouter } from 'react-router-dom'; -import chrome from 'ui/chrome'; -import { metadata } from 'ui/metadata'; - import { API_BASE_PATH } from '../../common/constants'; import { setDependencyCache } from '../shared_imports'; @@ -17,24 +14,20 @@ import { AppDependencies } from '../shim'; import { AuthorizationProvider } from './lib/authorization'; -const legacyBasePath = { - prepend: chrome.addBasePath, - get: chrome.getBasePath, - remove: () => {}, -}; -const legacyDocLinks = { - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: metadata.branch, -}; - let DependenciesContext: React.Context; const setAppDependencies = (deps: AppDependencies) => { + const legacyBasePath = { + prepend: deps.core.http.basePath.prepend, + get: deps.core.http.basePath.get, + remove: () => {}, + }; + setDependencyCache({ autocomplete: deps.plugins.data.autocomplete, - docLinks: legacyDocLinks as any, + docLinks: deps.core.docLinks, basePath: legacyBasePath as any, - XSRF: chrome.getXsrfToken(), + XSRF: deps.plugins.xsrfToken, }); DependenciesContext = createContext(deps); return DependenciesContext.Provider; @@ -48,6 +41,22 @@ export const useAppDependencies = () => { return useContext(DependenciesContext); }; +export const useDocumentationLinks = () => { + const { + core: { documentation }, + } = useAppDependencies(); + return documentation; +}; + +export const useToastNotifications = () => { + const { + core: { + notifications: { toasts: toastNotifications }, + }, + } = useAppDependencies(); + return toastNotifications; +}; + export const getAppProviders = (deps: AppDependencies) => { const I18nContext = deps.core.i18n.Context; @@ -55,9 +64,7 @@ export const getAppProviders = (deps: AppDependencies) => { const AppDependenciesProvider = setAppDependencies(deps); return ({ children }: { children: ReactNode }) => ( - + {children} diff --git a/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx b/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx index ac98d92fdba837..15966a93e1f429 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx +++ b/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx @@ -29,9 +29,9 @@ export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string } export const RedirectToTransformManagement: FC = () => ( - + ); export const RedirectToCreateTransform: FC<{ savedObjectId: string }> = ({ savedObjectId }) => ( - + ); diff --git a/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx b/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx index 2ad6f0870c140d..4bef917a91a18b 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { Fragment } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import React from 'react'; interface Props { title: React.ReactNode; - error: { - data: { - error: string; - cause?: string[]; - message?: string; - }; - }; + error: Error | null; actions?: JSX.Element; } @@ -25,25 +19,11 @@ export const SectionError: React.FunctionComponent = ({ actions, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error.data; + const errorMessage = error?.message ?? JSON.stringify(error, null, 2); return ( - {cause ? message || errorString :

{message || errorString}

} - {cause && ( - - -
    - {cause.map((causeMsg, i) => ( -
  • {causeMsg}
  • - ))} -
-
- )} +
{errorMessage}
{actions ? actions : null}
); diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx index 904d788b04e2c7..81af5c974fe04a 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx @@ -6,23 +6,44 @@ import React from 'react'; import { render } from '@testing-library/react'; + +import { KibanaContext } from '../lib/kibana'; +import { createPublicShim } from '../../shim'; +import { getAppProviders } from '../app_dependencies'; + import { ToastNotificationText } from './toast_notification_text'; +jest.mock('../../shared_imports'); + describe('ToastNotificationText', () => { test('should render the text as plain text', () => { + const Providers = getAppProviders(createPublicShim()); const props = { text: 'a short text message', }; - const { container } = render(); + const { container } = render( + + + + + + ); expect(container.textContent).toBe('a short text message'); }); test('should render the text within a modal', () => { + const Providers = getAppProviders(createPublicShim()); const props = { text: 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 characters. ', }; - const { container } = render(); + const { container } = render( + + + + + + ); expect(container.textContent).toBe( 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 ...View details' ); diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx index c79bf52a86642a..4e0a0a12558d82 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx @@ -18,12 +18,17 @@ import { import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { useAppDependencies } from '../app_dependencies'; + const MAX_SIMPLE_MESSAGE_LENGTH = 140; export const ToastNotificationText: FC<{ text: any }> = ({ text }) => { + const { + core: { overlays }, + } = useAppDependencies(); + if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) { return text; } @@ -43,7 +48,7 @@ export const ToastNotificationText: FC<{ text: any }> = ({ text }) => { }`; const openModal = () => { - const modal = npStart.core.overlays.openModal( + const modal = overlays.openModal( toMountPoint( modal.close()}> diff --git a/x-pack/legacy/plugins/transform/public/app/constants/index.ts b/x-pack/legacy/plugins/transform/public/app/constants/index.ts index 78b5f018dd782e..5d71980c83714a 100644 --- a/x-pack/legacy/plugins/transform/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/constants/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const CLIENT_BASE_PATH = '/management/elasticsearch/transform'; +export const CLIENT_BASE_PATH = '/management/elasticsearch/transform/'; export enum SECTION_SLUG { HOME = 'transform_management', diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/index.ts b/x-pack/legacy/plugins/transform/public/app/hooks/index.ts index 7981f560a525fa..a36550bcd8e570 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/index.ts @@ -9,3 +9,4 @@ export { useGetTransforms } from './use_get_transforms'; export { useDeleteTransforms } from './use_delete_transform'; export { useStartTransforms } from './use_start_transform'; export { useStopTransforms } from './use_stop_transform'; +export { useRequest } from './use_request'; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts index c71299eccb34d3..802599aaedd4f8 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts @@ -5,14 +5,12 @@ */ import { useAppDependencies } from '../app_dependencies'; - import { PreviewRequestBody, TransformId } from '../common'; - -import { http } from '../services/http_service'; +import { httpFactory, Http } from '../services/http_service'; import { EsIndex, TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; -const apiFactory = (basePath: string, indicesBasePath: string) => ({ +const apiFactory = (basePath: string, indicesBasePath: string, http: Http) => ({ getTransforms(transformId?: TransformId): Promise { const transformIdString = transformId !== undefined ? `/${transformId}` : ''; return http({ @@ -98,6 +96,8 @@ export const useApi = () => { const basePath = appDeps.core.http.basePath.prepend('/api/transform'); const indicesBasePath = appDeps.core.http.basePath.prepend('/api'); + const xsrfToken = appDeps.plugins.xsrfToken; + const http = httpFactory(xsrfToken); - return apiFactory(basePath, indicesBasePath); + return apiFactory(basePath, indicesBasePath, http); }; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx index e23151900447c2..83f456231cb856 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; @@ -17,6 +17,7 @@ import { useApi } from './use_api'; import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useDeleteTransforms = () => { + const toastNotifications = useToastNotifications(); const api = useApi(); return async (transforms: TransformListRow[]) => { @@ -54,7 +55,9 @@ export const useDeleteTransforms = () => { title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', }), - text: toMountPoint(), + text: toMountPoint( + + ), }); } }; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts new file mode 100644 index 00000000000000..8c489048a77ef8 --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_request.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 { UseRequestConfig, useRequest as _useRequest } from '../../shared_imports'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useRequest = (config: UseRequestConfig) => { + const { + core: { http }, + } = useAppDependencies(); + return _useRequest(http, config); +}; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts index d6b216accebd94..f460d8200c6e44 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts @@ -5,14 +5,15 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { useApi } from './use_api'; import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useStartTransforms = () => { + const toastNotifications = useToastNotifications(); const api = useApi(); return async (transforms: TransformListRow[]) => { diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts index bf6edf995bb1fe..758c574a3f7cdc 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts @@ -5,14 +5,15 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { useApi } from './use_api'; import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useStopTransforms = () => { + const toastNotifications = useToastNotifications(); const api = useApi(); return async (transforms: TransformListRow[]) => { diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index 8060659c78b80e..dde63710f56aae 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -5,20 +5,12 @@ */ import React, { createContext } from 'react'; -import { useRequest } from '../../../services/http/use_request'; +import { useRequest } from '../../../hooks'; import { hasPrivilegeFactory, Capabilities, Privileges } from './common'; -interface ApiError { - data: { - error: string; - cause?: string[]; - message?: string; - }; -} - interface Authorization { isLoading: boolean; - apiError: ApiError | null; + apiError: Error | null; privileges: Privileges; capabilities: Capabilities; } @@ -58,7 +50,7 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) = isLoading, privileges: isLoading ? { ...initialValue.privileges } : privilegesData, capabilities: { ...initialCapabalities }, - apiError: error ? (error as ApiError) : null, + apiError: error ? (error as Error) : null, }; const hasPrivilege = hasPrivilegeFactory(value.privileges); diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index b0a0371d2de86f..3acec1ea0e809c 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -22,7 +22,6 @@ export interface InitializedKibanaContextValue { combinedQuery: any; indexPatterns: IndexPatternsContract; initialized: true; - kbnBaseUrl: string; kibanaConfig: IUiSettingsClient; currentIndexPattern: IndexPattern; currentSavedSearch?: SavedSearch; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx index d2cf5f2b32910a..f2574a4a85f296 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx @@ -6,8 +6,6 @@ import React, { useEffect, useState, FC } from 'react'; -import { npStart } from 'ui/new_platform'; - import { useAppDependencies } from '../../app_dependencies'; import { @@ -19,15 +17,14 @@ import { import { InitializedKibanaContextValue, KibanaContext, KibanaContextValue } from './kibana_context'; -const indexPatterns = npStart.plugins.data.indexPatterns; -const savedObjectsClient = npStart.core.savedObjects.client; - interface Props { savedObjectId: string; } export const KibanaProvider: FC = ({ savedObjectId, children }) => { const appDeps = useAppDependencies(); + const indexPatterns = appDeps.plugins.data.indexPatterns; + const savedObjectsClient = appDeps.core.savedObjects.client; const savedSearches = appDeps.plugins.savedSearches.getClient(); const [contextValue, setContextValue] = useState({ initialized: false }); @@ -50,7 +47,7 @@ export const KibanaProvider: FC = ({ savedObjectId, children }) => { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } - const kibanaConfig = npStart.core.uiSettings; + const kibanaConfig = appDeps.core.uiSettings; const { indexPattern: currentIndexPattern, @@ -61,7 +58,6 @@ export const KibanaProvider: FC = ({ savedObjectId, children }) => { const kibanaContext: InitializedKibanaContextValue = { indexPatterns, initialized: true, - kbnBaseUrl: npStart.core.injectedMetadata.getBasePath(), kibanaConfig, combinedQuery, currentIndexPattern, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index de96a4de329623..8f58bc94e7c121 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -22,14 +22,13 @@ import { EuiTitle, } from '@elastic/eui'; -import { npStart } from 'ui/new_platform'; - import { useApi } from '../../hooks/use_api'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; + +import { useAppDependencies, useDocumentationLinks } from '../../app_dependencies'; import { TransformPivotConfig } from '../../common'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { documentationLinksService } from '../../services/documentation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { getIndexPatternIdByTitle, @@ -40,9 +39,6 @@ import { import { Wizard } from '../create_transform/components/wizard'; -const indexPatterns = npStart.plugins.data.indexPatterns; -const savedObjectsClient = npStart.core.savedObjects.client; - interface GetTransformsResponseOk { count: number; transforms: TransformPivotConfig[]; @@ -74,6 +70,12 @@ export const CloneTransformSection: FC = ({ match }) => { const api = useApi(); + const appDeps = useAppDependencies(); + const savedObjectsClient = appDeps.core.savedObjects.client; + const indexPatterns = appDeps.plugins.data.indexPatterns; + + const { esTransform } = useDocumentationLinks(); + const transformId = match.params.transformId; const [transformConfig, setTransformConfig] = useState(); @@ -154,7 +156,7 @@ export const CloneTransformSection: FC = ({ match }) => { { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index a0c91c070844bf..625c545ee8c46a 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -11,8 +11,6 @@ import { KibanaContext } from '../../../../lib/kibana'; import { StepCreateForm } from './step_create_form'; -jest.mock('ui/new_platform'); - // workaround to make React.memo() work with enzyme jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 2ca3253d72b449..312d8a30dab779 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -6,7 +6,6 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { EuiButton, @@ -30,13 +29,15 @@ import { } from '@elastic/eui'; import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { ToastNotificationText } from '../../../../components'; -import { useApi } from '../../../../hooks/use_api'; -import { useKibanaContext } from '../../../../lib/kibana'; -import { RedirectToTransformManagement } from '../../../../common/navigation'; + import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; import { getTransformProgress, getDiscoverUrl } from '../../../../common'; +import { useApi } from '../../../../hooks/use_api'; +import { useKibanaContext } from '../../../../lib/kibana'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { RedirectToTransformManagement } from '../../../../common/navigation'; +import { ToastNotificationText } from '../../../../components'; export interface StepDetailsExposedState { created: boolean; @@ -73,7 +74,9 @@ export const StepCreateForm: FC = React.memo( undefined ); + const deps = useAppDependencies(); const kibanaContext = useKibanaContext(); + const toastNotifications = useToastNotifications(); useEffect(() => { onChange({ created, started, indexPatternId }); @@ -437,7 +440,7 @@ export const StepCreateForm: FC = React.memo( defaultMessage: 'Use Discover to explore the transform.', } )} - href={getDiscoverUrl(indexPatternId, kibanaContext.kbnBaseUrl)} + href={getDiscoverUrl(indexPatternId, deps.core.http.basePath.get())} data-test-subj="transformWizardCardDiscover" /> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx index a2aa056c1634dd..2ac4295da1eedc 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx @@ -19,8 +19,6 @@ import { import { PivotPreview } from './pivot_preview'; -jest.mock('ui/new_platform'); - // workaround to make React.memo() work with enzyme jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 0311b26304c308..44edd1340e8d68 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -15,9 +15,7 @@ import { PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; -import { StepDefineForm, isAggNameConflict } from './step_define_form'; - -jest.mock('ui/new_platform'); +import { StepDefineForm, getAggNameConflictToastMessages } from './step_define_form'; // workaround to make React.memo() work with enzyme jest.mock('react', () => { @@ -76,38 +74,78 @@ describe('Transform: isAggNameConflict()', () => { }; // no conflict, completely different name, no namespacing involved - expect(isAggNameConflict('the-other-agg-name', aggList, groupByList)).toBe(false); + expect( + getAggNameConflictToastMessages('the-other-agg-name', aggList, groupByList) + ).toHaveLength(0); // no conflict, completely different name and no conflicting namespace - expect(isAggNameConflict('the-other-agg-name.namespace', aggList, groupByList)).toBe(false); + expect( + getAggNameConflictToastMessages('the-other-agg-name.namespace', aggList, groupByList) + ).toHaveLength(0); // exact match conflict on aggregation name - expect(isAggNameConflict('the-agg-name', aggList, groupByList)).toBe(true); + expect(getAggNameConflictToastMessages('the-agg-name', aggList, groupByList)).toStrictEqual([ + `An aggregation configuration with the name 'the-agg-name' already exists.`, + ]); // namespace conflict with `the-agg-name` aggregation - expect(isAggNameConflict('the-agg-name.namespace', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-agg-name.namespace', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-agg-name.namespace' because of a nesting conflict with 'the-agg-name'.`, + ]); // exact match conflict on group-by name - expect(isAggNameConflict('the-group-by-agg-name', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-group-by-agg-name', aggList, groupByList) + ).toStrictEqual([ + `A group by configuration with the name 'the-group-by-agg-name' already exists.`, + ]); // namespace conflict with `the-group-by-agg-name` group-by - expect(isAggNameConflict('the-group-by-agg-name.namespace', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-group-by-agg-name.namespace', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-group-by-agg-name.namespace' because of a nesting conflict with 'the-group-by-agg-name'.`, + ]); // exact match conflict on namespaced agg name - expect(isAggNameConflict('the-namespaced-agg-name.namespace', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-namespaced-agg-name.namespace', aggList, groupByList) + ).toStrictEqual([ + `An aggregation configuration with the name 'the-namespaced-agg-name.namespace' already exists.`, + ]); // no conflict, same base agg name but different namespace - expect(isAggNameConflict('the-namespaced-agg-name.namespace2', aggList, groupByList)).toBe( - false - ); + expect( + getAggNameConflictToastMessages('the-namespaced-agg-name.namespace2', aggList, groupByList) + ).toHaveLength(0); // namespace conflict because the new agg name is base name of existing nested field - expect(isAggNameConflict('the-namespaced-agg-name', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-namespaced-agg-name', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-namespaced-agg-name' because of a nesting conflict with 'the-namespaced-agg-name.namespace'.`, + ]); // exact match conflict on namespaced group-by name expect( - isAggNameConflict('the-namespaced-group-by-agg-name.namespace', aggList, groupByList) - ).toBe(true); + getAggNameConflictToastMessages( + 'the-namespaced-group-by-agg-name.namespace', + aggList, + groupByList + ) + ).toStrictEqual([ + `A group by configuration with the name 'the-namespaced-group-by-agg-name.namespace' already exists.`, + ]); // no conflict, same base group-by name but different namespace expect( - isAggNameConflict('the-namespaced-group-by-agg-name.namespace2', aggList, groupByList) - ).toBe(false); + getAggNameConflictToastMessages( + 'the-namespaced-group-by-agg-name.namespace2', + aggList, + groupByList + ) + ).toHaveLength(0); // namespace conflict because the new group-by name is base name of existing nested field - expect(isAggNameConflict('the-namespaced-group-by-agg-name', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-namespaced-group-by-agg-name', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-namespaced-group-by-agg-name' because of a nesting conflict with 'the-namespaced-group-by-agg-name.namespace'.`, + ]); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 1499f99f828248..3adb74e4704dcd 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -9,9 +9,6 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { metadata } from 'ui/metadata'; -import { toastNotifications } from 'ui/notify'; - import { EuiButton, EuiCodeEditor, @@ -29,6 +26,7 @@ import { } from '@elastic/eui'; import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode'; +import { useDocumentationLinks, useToastNotifications } from '../../../../app_dependencies'; import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; import { DropDown } from '../aggregation_dropdown'; @@ -147,32 +145,30 @@ export function applyTransformConfigToDefineState( return state; } -export function isAggNameConflict( +export function getAggNameConflictToastMessages( aggName: AggName, aggList: PivotAggsConfigDict, groupByList: PivotGroupByConfigDict -) { +): string[] { if (aggList[aggName] !== undefined) { - toastNotifications.addDanger( + return [ i18n.translate('xpack.transform.stepDefineForm.aggExistsErrorMessage', { defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, values: { aggName }, - }) - ); - return true; + }), + ]; } if (groupByList[aggName] !== undefined) { - toastNotifications.addDanger( + return [ i18n.translate('xpack.transform.stepDefineForm.groupByExistsErrorMessage', { defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, values: { aggName }, - }) - ); - return true; + }), + ]; } - let conflict = false; + const conflicts: string[] = []; // check the new aggName against existing aggs and groupbys const aggNameSplit = aggName.split('.'); @@ -180,29 +176,28 @@ export function isAggNameConflict( aggNameSplit.forEach(aggNamePart => { aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { - toastNotifications.addDanger( + conflicts.push( i18n.translate('xpack.transform.stepDefineForm.nestedConflictErrorMessage', { defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggNameCheck}'.`, values: { aggName, aggNameCheck }, }) ); - conflict = true; } }); - if (conflict) { - return true; + if (conflicts.length > 0) { + return conflicts; } // check all aggs against new aggName - conflict = Object.keys(aggList).some(aggListName => { + Object.keys(aggList).some(aggListName => { const aggListNameSplit = aggListName.split('.'); let aggListNameCheck: string; return aggListNameSplit.some(aggListNamePart => { aggListNameCheck = aggListNameCheck === undefined ? aggListNamePart : `${aggListNameCheck}.${aggListNamePart}`; if (aggListNameCheck === aggName) { - toastNotifications.addDanger( + conflicts.push( i18n.translate('xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage', { defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggListName}'.`, values: { aggName, aggListName }, @@ -214,12 +209,12 @@ export function isAggNameConflict( }); }); - if (conflict) { - return true; + if (conflicts.length > 0) { + return conflicts; } // check all group-bys against new aggName - conflict = Object.keys(groupByList).some(groupByListName => { + Object.keys(groupByList).some(groupByListName => { const groupByListNameSplit = groupByListName.split('.'); let groupByListNameCheck: string; return groupByListNameSplit.some(groupByListNamePart => { @@ -228,7 +223,7 @@ export function isAggNameConflict( ? groupByListNamePart : `${groupByListNameCheck}.${groupByListNamePart}`; if (groupByListNameCheck === aggName) { - toastNotifications.addDanger( + conflicts.push( i18n.translate('xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage', { defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{groupByListName}'.`, values: { aggName, groupByListName }, @@ -240,7 +235,7 @@ export function isAggNameConflict( }); }); - return conflict; + return conflicts; } interface Props { @@ -250,6 +245,8 @@ interface Props { export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange }) => { const kibanaContext = useKibanaContext(); + const toastNotifications = useToastNotifications(); + const { esQueryDsl, esTransformPivot } = useDocumentationLinks(); const defaults = { ...getDefaultStepDefineState(kibanaContext), ...overrides }; @@ -288,7 +285,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const config: PivotGroupByConfig = groupByOptionsData[label]; const aggName: AggName = config.aggName; - if (isAggNameConflict(aggName, aggList, groupByList)) { + const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -300,7 +299,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const groupByListWithoutPrevious = { ...groupByList }; delete groupByListWithoutPrevious[previousAggName]; - if (isAggNameConflict(item.aggName, aggList, groupByListWithoutPrevious)) { + const aggNameConflictMessages = getAggNameConflictToastMessages( + item.aggName, + aggList, + groupByListWithoutPrevious + ); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -321,7 +326,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const config: PivotAggsConfig = aggOptionsData[label]; const aggName: AggName = config.aggName; - if (isAggNameConflict(aggName, aggList, groupByList)) { + const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -333,7 +340,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const aggListWithoutPrevious = { ...aggList }; delete aggListWithoutPrevious[previousAggName]; - if (isAggNameConflict(item.aggName, aggListWithoutPrevious, groupByList)) { + const aggNameConflictMessages = getAggNameConflictToastMessages( + item.aggName, + aggListWithoutPrevious, + groupByList + ); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -477,15 +490,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange setAdvancedSourceEditorApplyButtonEnabled(false); }; - // metadata.branch corresponds to the version used in documentation links. - const docsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/transform-resource.html#transform-pivot`; const advancedEditorHelpText = ( {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { defaultMessage: 'The advanced editor allows you to edit the pivot configuration of the transform.', })}{' '} - + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { defaultMessage: 'Learn more about available options.', })} @@ -493,14 +504,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange ); - const sourceDocsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/query-dsl.html`; const advancedSourceEditorHelpText = ( {i18n.translate('xpack.transform.stepDefineForm.advancedSourceEditorHelpText', { defaultMessage: 'The advanced editor allows you to edit the source query clause of the transform.', })}{' '} - + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { defaultMessage: 'Learn more about available options.', })} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index aae366e6008d55..78f6fc30f91911 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -18,8 +18,6 @@ import { import { StepDefineExposedState } from './step_define_form'; import { StepDefineSummary } from './step_define_summary'; -jest.mock('ui/new_platform'); - // workaround to make React.memo() work with enzyme jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 220923f88ed363..5ae2180bfe7790 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -7,8 +7,6 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { metadata } from 'ui/metadata'; -import { toastNotifications } from 'ui/notify'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; @@ -16,6 +14,7 @@ import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_r import { useKibanaContext } from '../../../../lib/kibana'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; +import { useDocumentationLinks, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; import { useApi } from '../../../../hooks/use_api'; @@ -72,6 +71,8 @@ interface Props { export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange }) => { const kibanaContext = useKibanaContext(); + const toastNotifications = useToastNotifications(); + const { esIndicesCreateIndex } = useDocumentationLinks(); const defaults = { ...getDefaultStepDetailsState(), ...overrides }; @@ -274,10 +275,7 @@ export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange defaultMessage: 'Invalid destination index name.', })}
- + {i18n.translate( 'xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink', { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index ae82f03718c02f..e92ba256256a4a 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -22,8 +22,9 @@ import { } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; + +import { useDocumentationLinks } from '../../app_dependencies'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { documentationLinksService } from '../../services/documentation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { KibanaProvider, RenderOnlyWithInitializedKibanaContext } from '../../lib/kibana'; @@ -37,6 +38,8 @@ export const CreateTransformSection: FC = ({ match }) => { docTitleService.setTitle('createTransform'); }, []); + const { esTransform } = useDocumentationLinks(); + return ( @@ -65,7 +68,7 @@ export const CreateTransformSection: FC = ({ match }) => { ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx index 40098ac7ef72ad..4b333f73f048c1 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx @@ -30,7 +30,7 @@ export const CloneAction: FC = ({ itemId }) => { }); function clickHandler() { - history.push(`${CLIENT_BASE_PATH}/${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`); + history.push(`${CLIENT_BASE_PATH}${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`); } const cloneButton = ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx index 4795a2eb7d7bc2..82b9f0a292bb97 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx @@ -16,7 +16,6 @@ import { DeleteAction } from './action_delete'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx index 5f4d4a71c71eba..002b4ea19f9678 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx @@ -16,7 +16,6 @@ import { StartAction } from './action_start'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx index f6bb1c8b60667a..e2a22765dfb981 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx @@ -16,7 +16,6 @@ import { StopAction } from './action_stop'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx index 12e1ba5528c43a..e8ac2fa057ad8e 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx @@ -6,8 +6,6 @@ import { getActions } from './actions'; -jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx index 42f04ed101ad6b..b4198ce3c7244d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx @@ -6,8 +6,6 @@ import { getColumns } from './columns'; -jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Job List Columns', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts deleted file mode 100644 index 1d20965526115f..00000000000000 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { chromeServiceMock } from '../../../../../../../../../../src/core/public/mocks'; - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index e1a19ddd3c742e..5e0363d0a7a152 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -7,11 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import './transform_list.test.mocks'; import { TransformList } from './transform_list'; -jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts index 8e505c7cccc022..812e636a3b338c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts @@ -6,12 +6,7 @@ import React, { useEffect } from 'react'; -import { timefilter } from 'ui/timefilter'; - -import { - DEFAULT_REFRESH_INTERVAL_MS, - MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../../common/constants'; +import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; import { useRefreshTransformList } from '../../../../common'; @@ -20,62 +15,11 @@ export const useRefreshInterval = ( ) => { const { refresh } = useRefreshTransformList(); useEffect(() => { - let transformRefreshInterval: null | number = null; - const refreshIntervalSubscription = timefilter - .getRefreshIntervalUpdate$() - .subscribe(setAutoRefresh); - - timefilter.disableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - - initAutoRefresh(); - - function initAutoRefresh() { - const { value } = timefilter.getRefreshInterval(); - if (value === 0) { - // the auto refresher starts in an off state - // so switch it on and set the interval to 30s - timefilter.setRefreshInterval({ - pause: false, - value: DEFAULT_REFRESH_INTERVAL_MS, - }); - } - - setAutoRefresh(); - } - - function setAutoRefresh() { - const { value, pause } = timefilter.getRefreshInterval(); - if (pause) { - clearRefreshInterval(); - } else { - setRefreshInterval(value); - } - refresh(); - } - - function setRefreshInterval(interval: number) { - clearRefreshInterval(); - if (interval >= MINIMUM_REFRESH_INTERVAL_MS) { - setBlockRefresh(false); - const intervalId = window.setInterval(() => { - refresh(); - }, interval); - transformRefreshInterval = intervalId; - } - } - - function clearRefreshInterval() { - setBlockRefresh(true); - if (transformRefreshInterval !== null) { - window.clearInterval(transformRefreshInterval); - } - } + const interval = setInterval(refresh, DEFAULT_REFRESH_INTERVAL_MS); // useEffect cleanup return () => { - refreshIntervalSubscription.unsubscribe(); - clearRefreshInterval(); + clearInterval(interval); }; // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx index f68670f0b38b2b..b1ca9e370c99e3 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx @@ -9,11 +9,6 @@ import React from 'react'; import { TransformManagementSection } from './transform_management_section'; -jest.mock('ui/new_platform'); -jest.mock('ui/timefilter', () => { - return {}; -}); - jest.mock('../../../shared_imports'); describe('Transform: ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 6eb03c6537e0e1..1573d4c53c0cf4 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -24,12 +24,12 @@ import { } from '@elastic/eui'; import { APP_GET_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { useDocumentationLinks } from '../../app_dependencies'; import { useRefreshTransformList, TransformListRow } from '../../common'; import { useGetTransforms } from '../../hooks'; import { RedirectToCreateTransform } from '../../common/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { documentationLinksService } from '../../services/documentation'; import { useRefreshInterval } from './components/transform_list/use_refresh_interval'; import { SearchSelection } from './components/search_selection'; @@ -37,6 +37,8 @@ import { TransformList } from './components/transform_list'; import { TransformStatsBar } from './components/transform_list/transforms_stats_bar'; export const TransformManagement: FC = () => { + const { esTransform } = useDocumentationLinks(); + const [transformsLoading, setTransformsLoading] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [blockRefresh, setBlockRefresh] = useState(false); @@ -98,7 +100,7 @@ export const TransformManagement: FC = () => { > => { - return _sendRequest(httpService.httpClient, config); -}; - -export const useRequest = (config: UseRequestConfig) => { - return _useRequest(httpService.httpClient, config); -}; diff --git a/x-pack/legacy/plugins/transform/public/app/services/http_service.ts b/x-pack/legacy/plugins/transform/public/app/services/http_service.ts index 11ba5119b1394d..fa4c8d1ba78447 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/http_service.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/http_service.ts @@ -5,48 +5,47 @@ */ // service for interacting with the server - -import chrome from 'ui/chrome'; - -// @ts-ignore -import { addSystemApiHeader } from 'ui/system_api'; - import { Dictionary } from '../../../common/types/common'; -export function http(options: Dictionary) { - 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: Dictionary = { - method: options.method || 'GET', - headers: allHeaders, - credentials: 'same-origin', - }; - - if (body !== null) { - payload.body = body; +export type Http = (options: Dictionary) => Promise; + +export function httpFactory(xsrfToken: string) { + return function http(options: Dictionary) { + return new Promise((resolve, reject) => { + if (options && options.url) { + let url = ''; + url = url + (options.url || ''); + const headers = { + 'kbn-system-request': true, + 'Content-Type': 'application/json', + 'kbn-version': xsrfToken, + ...options.headers, + }; + + const allHeaders = + options.headers === undefined ? headers : { ...options.headers, ...headers }; + const body = options.data === undefined ? null : JSON.stringify(options.data); + + const payload: Dictionary = { + 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(); } - - 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/transform/public/app/services/navigation/breadcrumb.ts b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts index 5a2f698b351544..aa8041a1cbe239 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts @@ -7,6 +7,10 @@ import { textService } from '../text'; import { linkToHome } from './links'; +import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + export enum BREADCRUMB_SECTION { MANAGEMENT = 'management', HOME = 'home', @@ -24,17 +28,16 @@ type Breadcrumbs = { }; class BreadcrumbService { - private chrome: any; private breadcrumbs: Breadcrumbs = { management: [], home: [], cloneTransform: [], createTransform: [], }; + private setBreadcrumbsHandler?: SetBreadcrumbs; - public init(chrome: any, managementBreadcrumb: any): void { - this.chrome = chrome; - this.breadcrumbs.management = [managementBreadcrumb]; + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; // Home and sections this.breadcrumbs.home = [ @@ -59,12 +62,19 @@ class BreadcrumbService { } public setBreadcrumbs(type: BREADCRUMB_SECTION): void { + if (!this.setBreadcrumbsHandler) { + throw new Error(`BreadcrumbService#setup() must be called first!`); + } + const newBreadcrumbs = this.breadcrumbs[type] ? [...this.breadcrumbs[type]] : [...this.breadcrumbs.home]; // Pop off last breadcrumb - const lastBreadcrumb = newBreadcrumbs.pop() as BreadcrumbItem; + const lastBreadcrumb = newBreadcrumbs.pop() as { + text: string; + href?: string; + }; // Put last breadcrumb back without href newBreadcrumbs.push({ @@ -72,7 +82,7 @@ class BreadcrumbService { href: undefined, }); - this.chrome.setBreadcrumbs(newBreadcrumbs); + this.setBreadcrumbsHandler(newBreadcrumbs); } } diff --git a/x-pack/legacy/plugins/transform/public/plugin.ts b/x-pack/legacy/plugins/transform/public/plugin.ts index d55695f891bb7f..23fad00fb0786f 100644 --- a/x-pack/legacy/plugins/transform/public/plugin.ts +++ b/x-pack/legacy/plugins/transform/public/plugin.ts @@ -3,121 +3,89 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { PLUGIN } from '../common/constants'; -import { CLIENT_BASE_PATH } from './app/constants'; -import { renderReact } from './app/app'; -import { Core, Plugins } from './shim'; +import { renderApp } from './app/app'; +import { ShimCore, ShimPlugins } from './shim'; -import { breadcrumbService, docTitleService } from './app/services/navigation'; -import { documentationLinksService } from './app/services/documentation'; -import { httpService } from './app/services/http'; +import { breadcrumbService } from './app/services/navigation'; +import { docTitleService } from './app/services/navigation'; import { textService } from './app/services/text'; import { uiMetricService } from './app/services/ui_metric'; import { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; -const REACT_ROOT_ID = 'transformReactRoot'; -const KBN_MANAGEMENT_SECTION = 'elasticsearch/transform'; - -const template = `
`; - export class Plugin { - public start(core: Core, plugins: Plugins): void { + public start(core: ShimCore, plugins: ShimPlugins): void { const { http, - routing, - legacyHttp, chrome, documentation, + docLinks, docTitle, + injectedMetadata, + notifications, uiSettings, savedObjects, overlays, } = core; - const { data, management, savedSearches: coreSavedSearches, uiMetric } = plugins; + const { data, management, savedSearches: coreSavedSearches, uiMetric, xsrfToken } = plugins; // AppCore/AppPlugins to be passed on as React context const appDependencies = { - core: { chrome, http, i18n: core.i18n, uiSettings, savedObjects, overlays }, + core: { + chrome, + documentation, + docLinks, + http, + i18n: core.i18n, + injectedMetadata, + notifications, + uiSettings, + savedObjects, + overlays, + }, plugins: { data, - management: { sections: management.sections }, + management, savedSearches: coreSavedSearches, + xsrfToken, }, }; // Register management section const esSection = management.sections.getSection('elasticsearch'); - esSection.register(PLUGIN.ID, { - visible: true, - display: i18n.translate('xpack.transform.appName', { - defaultMessage: 'Transforms', - }), - order: 3, - url: `#${CLIENT_BASE_PATH}`, - }); + if (esSection !== undefined) { + esSection.registerApp({ + id: 'transform', + title: i18n.translate('xpack.transform.appTitle', { + defaultMessage: 'Transforms', + }), + order: 3, + mount(params) { + const savedSearches = createSavedSearchesLoader({ + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + chrome: core.chrome, + overlays: core.overlays, + }); + coreSavedSearches.setClient(savedSearches); + + breadcrumbService.setup(params.setBreadcrumbs); + params.setBreadcrumbs([ + { + text: i18n.translate('xpack.transform.breadcrumbsTitle', { + defaultMessage: 'Transforms', + }), + }, + ]); + + return renderApp(params.element, appDependencies); + }, + }); + } // Initialize services textService.init(); - breadcrumbService.init(chrome, management.constants.BREADCRUMB); uiMetricService.init(uiMetric.createUiStatsReporter); - documentationLinksService.init(documentation.esDocBasePath); docTitleService.init(docTitle.change); - - const unmountReactApp = (): void => { - const elem = document.getElementById(REACT_ROOT_ID); - if (elem) { - unmountComponentAtNode(elem); - } - }; - - // Register react root - routing.registerAngularRoute(`${CLIENT_BASE_PATH}/:section?/:subsection?/:view?/:id?`, { - template, - controllerAs: 'transformController', - controller: ($scope: any, $route: any, $http: ng.IHttpService) => { - const savedSearches = createSavedSearchesLoader({ - savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - chrome: core.chrome, - overlays: core.overlays, - }); - // NOTE: We depend upon Angular's $http service because it's decorated with interceptors, - // e.g. to check license status per request. - legacyHttp.setClient($http); - httpService.init(legacyHttp.getClient()); - coreSavedSearches.setClient(savedSearches); - - // Angular Lifecycle - const appRoute = $route.current; - const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template; - - // When we navigate within Transform, prevent Angular from re-matching the route and rebuild the app - if (isNavigationInApp) { - $route.current = appRoute; - } else { - // Any clean up when user leaves Transform - } - - $scope.$on('$destroy', () => { - if (stopListeningForLocationChange) { - stopListeningForLocationChange(); - } - unmountReactApp(); - }); - }); - - $scope.$$postDigest(() => { - unmountReactApp(); - const elem = document.getElementById(REACT_ROOT_ID); - if (elem) { - renderReact(elem, appDependencies); - } - }); - }, - }); } } diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts index 248eb00c67dff6..b077cd8836c4b5 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts @@ -16,7 +16,7 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/request'; +} from '../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { CronEditor, diff --git a/x-pack/legacy/plugins/transform/public/shim.ts b/x-pack/legacy/plugins/transform/public/shim.ts index 38bb072ff9eb7c..95f54605377a86 100644 --- a/x-pack/legacy/plugins/transform/public/shim.ts +++ b/x-pack/legacy/plugins/transform/public/shim.ts @@ -6,77 +6,69 @@ import { npStart } from 'ui/new_platform'; -import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; -import routes from 'ui/routes'; +import chrome from 'ui/chrome'; import { docTitle } from 'ui/doc_title/doc_title'; -import { CoreStart } from 'kibana/public'; // @ts-ignore: allow traversal to fail on x-pack build import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; import { SavedSearchLoader } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/types'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -export type npCore = typeof npStart.core; +import { TRANSFORM_DOC_PATHS } from './app/constants'; + +export type NpCore = typeof npStart.core; +export type NpPlugins = typeof npStart.plugins; // AppCore/AppPlugins is the set of core features/plugins // we pass on via context/hooks to the app and its components. export type AppCore = Pick< - CoreStart, - 'chrome' | 'http' | 'i18n' | 'savedObjects' | 'uiSettings' | 'overlays' + ShimCore, + | 'chrome' + | 'documentation' + | 'docLinks' + | 'http' + | 'i18n' + | 'injectedMetadata' + | 'savedObjects' + | 'uiSettings' + | 'overlays' + | 'notifications' >; - -export interface AppPlugins { - data: DataPublicPluginStart; - management: { - sections: typeof management; - }; - savedSearches: { - getClient(): any; - setClient(client: any): void; - }; -} +export type AppPlugins = Pick; export interface AppDependencies { core: AppCore; plugins: AppPlugins; } -export interface Core extends npCore { - legacyHttp: { - getClient(): any; - setClient(client: any): void; - }; - routing: { - registerAngularRoute(path: string, config: object): void; - }; - documentation: { - esDocBasePath: string; - esPluginDocBasePath: string; - esStackOverviewDocBasePath: string; - esMLDocBasePath: string; - }; +export interface ShimCore extends NpCore { + documentation: Record< + | 'esDocBasePath' + | 'esIndicesCreateIndex' + | 'esPluginDocBasePath' + | 'esQueryDsl' + | 'esStackOverviewDocBasePath' + | 'esTransform' + | 'esTransformPivot' + | 'mlDocBasePath', + string + >; docTitle: { change: typeof docTitle.change; }; } -export interface Plugins extends AppPlugins { - management: { - sections: typeof management; - constants: { - BREADCRUMB: typeof MANAGEMENT_BREADCRUMB; - }; - }; +export interface ShimPlugins extends NpPlugins { uiMetric: { createUiStatsReporter: typeof createUiStatsReporter; }; - data: DataPublicPluginStart; + savedSearches: { + getClient(): any; + setClient(client: any): void; + }; + xsrfToken: string; } -export function createPublicShim(): { core: Core; plugins: Plugins } { - // This is an Angular service, which is why we use this provider pattern - // to access it within our React app. - let httpClient: ng.IHttpService; +export function createPublicShim(): { core: ShimCore; plugins: ShimPlugins } { // This is an Angular service, which is why we use this provider pattern // to access it within our React app. let savedSearches: SavedSearchLoader; @@ -86,35 +78,22 @@ export function createPublicShim(): { core: Core; plugins: Plugins } { return { core: { ...npStart.core, - routing: { - registerAngularRoute: (path: string, config: object): void => { - routes.when(path, config); - }, - }, - legacyHttp: { - setClient: (client: any): void => { - httpClient = client; - }, - getClient: (): any => httpClient, - }, documentation: { esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`, + esIndicesCreateIndex: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/indices-create-index.html#indices-create-index`, esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`, + esQueryDsl: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/query-dsl.html`, esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`, - esMLDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, + esTransform: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/${TRANSFORM_DOC_PATHS.transforms}`, + esTransformPivot: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/put-transform.html#put-transform-request-body`, + mlDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, }, docTitle: { change: docTitle.change, }, }, plugins: { - data: npStart.plugins.data, - management: { - sections: management, - constants: { - BREADCRUMB: MANAGEMENT_BREADCRUMB, - }, - }, + ...npStart.plugins, savedSearches: { setClient: (client: any): void => { savedSearches = client; @@ -124,6 +103,7 @@ export function createPublicShim(): { core: Core; plugins: Plugins } { uiMetric: { createUiStatsReporter, }, + xsrfToken: chrome.getXsrfToken(), }, }; } From 3f7abe3c55023a546f559b536c232054be6d1d21 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Fri, 28 Feb 2020 07:28:31 -0600 Subject: [PATCH 32/34] Add alt attribute to images on the Add data page (#58767) * add alt attr to images * add alt attr to images --- .../components/__snapshots__/synopsis.test.js.snap | 1 + .../kibana/public/home/np_ready/components/synopsis.js | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap index 525cc5bdda9d42..594d67d9c8eb01 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap @@ -9,6 +9,7 @@ exports[`props iconType 1`] = ` href="link_to_item" icon={ diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js index 968b8eb64def5c..f43c377b4e5b90 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js @@ -37,13 +37,7 @@ export function Synopsis({ if (iconUrl) { optionalImg = ; } else if (iconType) { - optionalImg = ( - - ); + optionalImg = ; } const classes = classNames('homSynopsis__card', { From 91330d24933345e63dad8357a52d3a03168dba7f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 28 Feb 2020 09:06:48 -0500 Subject: [PATCH 33/34] Spaces - NP updates for usage collection and capabilities (#57693) * remove kibanaIndex from LegacyAPI * moving capabilities, adding tests * moving usage collection * cleanup * don't toggle capabilities on unauthenticated routes * reintroduce exception handling * pipe dat config * start addressing PR feedback * fix CoreSetup's generic type * fix usage collector tests * PR review updates Co-authored-by: Elastic Machine --- src/core/server/http/http_server.mocks.ts | 6 +- src/plugins/usage_collection/server/mocks.ts | 38 +++++++ x-pack/legacy/plugins/spaces/index.ts | 18 --- x-pack/plugins/features/server/index.ts | 2 +- x-pack/plugins/features/server/mocks.ts | 27 +++++ x-pack/plugins/features/server/plugin.ts | 23 ++-- .../capabilities_provider.test.ts | 24 ++++ .../capabilities/capabilities_provider.ts | 16 +++ .../capabilities_switcher.test.ts} | 104 ++++++++++++++++-- .../capabilities_switcher.ts} | 40 +++++-- .../spaces/server/capabilities/index.ts | 20 ++++ .../on_post_auth_interceptor.test.ts | 2 - .../on_post_auth_interceptor.ts | 4 +- .../on_request_interceptor.test.ts | 5 - .../on_request_interceptor.ts | 4 +- .../spaces_tutorial_context_factory.test.ts | 1 - x-pack/plugins/spaces/server/plugin.test.ts | 72 ++++++++++++ x-pack/plugins/spaces/server/plugin.ts | 63 +++++------ .../api/__fixtures__/create_legacy_api.ts | 3 - .../spaces_service/spaces_service.test.ts | 1 - .../spaces/server/usage_collection/index.ts | 7 ++ .../spaces_usage_collector.test.ts | 25 ++++- .../spaces_usage_collector.ts | 23 ++-- 23 files changed, 409 insertions(+), 119 deletions(-) create mode 100644 src/plugins/usage_collection/server/mocks.ts create mode 100644 x-pack/plugins/features/server/mocks.ts create mode 100644 x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts create mode 100644 x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts rename x-pack/plugins/spaces/server/{lib/toggle_ui_capabilities.test.ts => capabilities/capabilities_switcher.test.ts} (53%) rename x-pack/plugins/spaces/server/{lib/toggle_ui_capabilities.ts => capabilities/capabilities_switcher.ts} (66%) create mode 100644 x-pack/plugins/spaces/server/capabilities/index.ts create mode 100644 x-pack/plugins/spaces/server/plugin.test.ts create mode 100644 x-pack/plugins/spaces/server/usage_collection/index.ts rename x-pack/plugins/spaces/server/{lib => usage_collection}/spaces_usage_collector.test.ts (85%) rename x-pack/plugins/spaces/server/{lib => usage_collection}/spaces_usage_collector.ts (90%) diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index c586cf6a9825fa..0a9541393284e3 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -43,6 +43,7 @@ interface RequestFixtureOptions

{ method?: RouteMethod; socket?: Socket; routeTags?: string[]; + routeAuthRequired?: false; validation?: { params?: RouteValidationSpec

; query?: RouteValidationSpec; @@ -59,6 +60,7 @@ function createKibanaRequestMock

({ method = 'get', socket = new Socket(), routeTags, + routeAuthRequired, validation = {}, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); @@ -77,7 +79,9 @@ function createKibanaRequestMock

({ query: queryString, search: queryString ? `?${queryString}` : queryString, }, - route: { settings: { tags: routeTags } }, + route: { + settings: { tags: routeTags, auth: routeAuthRequired }, + }, raw: { req: { socket }, }, diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts new file mode 100644 index 00000000000000..2194b1fb83f6e8 --- /dev/null +++ b/src/plugins/usage_collection/server/mocks.ts @@ -0,0 +1,38 @@ +/* + * 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 { loggingServiceMock } from '../../../core/server/mocks'; +import { UsageCollectionSetup } from './plugin'; +import { CollectorSet } from './collector'; + +const createSetupContract = () => { + return { + ...new CollectorSet({ + logger: loggingServiceMock.createLogger(), + maximumWaitTimeForAllCollectorsInS: 1, + }), + registerLegacySavedObjects: jest.fn() as jest.Mocked< + UsageCollectionSetup['registerLegacySavedObjects'] + >, + } as UsageCollectionSetup; +}; + +export const usageCollectionPluginMock = { + createSetupContract, +}; diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index ab3388ae964759..757c1eb557c543 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -34,19 +34,6 @@ export const spaces = (kibana: Record) => publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - uiCapabilities() { - return { - spaces: { - manage: true, - }, - management: { - kibana: { - spaces: true, - }, - }, - }; - }, - uiExports: { styleSheetPaths: resolve(__dirname, 'public/index.scss'), managementSections: [], @@ -110,14 +97,9 @@ export const spaces = (kibana: Record) => throw new Error('New Platform XPack Spaces plugin is not available.'); } - const config = server.config(); - const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat; registerLegacyAPI({ - legacyConfig: { - kibanaIndex: config.get('kibana.index'), - }, savedObjects: server.savedObjects, auditLogger: { create: (pluginId: string) => diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 2b4f85aa04f046..48ef97a494f7ea 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -14,7 +14,7 @@ import { Plugin } from './plugin'; export { uiCapabilitiesRegex } from './feature_schema'; export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common'; -export { PluginSetupContract } from './plugin'; +export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/features/server/mocks.ts b/x-pack/plugins/features/server/mocks.ts new file mode 100644 index 00000000000000..ebaa5f1a504ca9 --- /dev/null +++ b/x-pack/plugins/features/server/mocks.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 { PluginSetupContract, PluginStartContract } from './plugin'; + +const createSetup = (): jest.Mocked => { + return { + getFeatures: jest.fn(), + getFeaturesUICapabilities: jest.fn(), + registerFeature: jest.fn(), + registerLegacyAPI: jest.fn(), + }; +}; + +const createStart = (): jest.Mocked => { + return { + getFeatures: jest.fn(), + }; +}; + +export const featuresPluginMock = { + createSetup, + createStart, +}; diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 96a8e68f8326d5..e77fa218c06815 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -30,6 +30,10 @@ export interface PluginSetupContract { registerLegacyAPI: (legacyAPI: LegacyAPI) => void; } +export interface PluginStartContract { + getFeatures(): Feature[]; +} + /** * Describes a set of APIs that are available in the legacy platform only and required by this plugin * to function properly. @@ -45,6 +49,8 @@ export interface LegacyAPI { export class Plugin { private readonly logger: Logger; + private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); + private legacyAPI?: LegacyAPI; private readonly getLegacyAPI = () => { if (!this.legacyAPI) { @@ -61,18 +67,16 @@ export class Plugin { core: CoreSetup, { timelion }: { timelion?: TimelionSetupContract } ): Promise> { - const featureRegistry = new FeatureRegistry(); - defineRoutes({ router: core.http.createRouter(), - featureRegistry, + featureRegistry: this.featureRegistry, getLegacyAPI: this.getLegacyAPI, }); return deepFreeze({ - registerFeature: featureRegistry.register.bind(featureRegistry), - getFeatures: featureRegistry.getAll.bind(featureRegistry), - getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(featureRegistry.getAll()), + registerFeature: this.featureRegistry.register.bind(this.featureRegistry), + getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()), registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; @@ -82,14 +86,17 @@ export class Plugin { savedObjectTypes: this.legacyAPI.savedObjectTypes, includeTimelion: timelion !== undefined && timelion.uiEnabled, })) { - featureRegistry.register(feature); + this.featureRegistry.register(feature); } }, }); } - public start() { + public start(): RecursiveReadonly { this.logger.debug('Starting plugin'); + return deepFreeze({ + getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + }); } public stop() { diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts new file mode 100644 index 00000000000000..8678bdceb70f9b --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { capabilitiesProvider } from './capabilities_provider'; + +describe('Capabilities provider', () => { + it('provides the expected capabilities', () => { + expect(capabilitiesProvider()).toMatchInlineSnapshot(` + Object { + "management": Object { + "kibana": Object { + "spaces": true, + }, + }, + "spaces": Object { + "manage": true, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts new file mode 100644 index 00000000000000..5976aabfa66e8a --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.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. + */ + +export const capabilitiesProvider = () => ({ + spaces: { + manage: true, + }, + management: { + kibana: { + spaces: true, + }, + }, +}); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts similarity index 53% rename from x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index b92922def2eb81..3f7b93c754aef3 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -6,8 +6,12 @@ import { Feature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; -import { toggleUICapabilities } from './toggle_ui_capabilities'; -import { Capabilities } from 'src/core/public'; +import { setupCapabilitiesSwitcher } from './capabilities_switcher'; +import { Capabilities, CoreSetup } from 'src/core/server'; +import { coreMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { featuresPluginMock } from '../../../features/server/mocks'; +import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; +import { PluginsStart } from '../plugin'; const features: Feature[] = [ { @@ -91,8 +95,33 @@ const buildCapabilities = () => }, }) as Capabilities; -describe('toggleUiCapabilities', () => { - it('does not toggle capabilities when the space has no disabled features', () => { +const setup = (space: Space) => { + const coreSetup = coreMock.createSetup(); + + const featuresStart = featuresPluginMock.createStart(); + featuresStart.getFeatures.mockReturnValue(features); + + coreSetup.getStartServices.mockResolvedValue([ + coreMock.createStart(), + { features: featuresStart }, + ]); + + const spacesService = spacesServiceMock.createSetupContract(); + spacesService.getActiveSpace.mockResolvedValue(space); + + const logger = loggingServiceMock.createLogger(); + + const switcher = setupCapabilitiesSwitcher( + (coreSetup as unknown) as CoreSetup, + spacesService, + logger + ); + + return { switcher, logger, spacesService }; +}; + +describe('capabilitiesSwitcher', () => { + it('does not toggle capabilities when the space has no disabled features', async () => { const space: Space = { id: 'space', name: '', @@ -100,11 +129,54 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); + + expect(result).toEqual(buildCapabilities()); + }); + + it('does not toggle capabilities when the request is not authenticated', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + }; + + const capabilities = buildCapabilities(); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest({ routeAuthRequired: false }); + + const result = await switcher(request, capabilities); + + expect(result).toEqual(buildCapabilities()); + }); + + it('logs a warning, and does not toggle capabilities if an error is encountered', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + }; + + const capabilities = buildCapabilities(); + + const { switcher, logger, spacesService } = setup(space); + const request = httpServerMock.createKibanaRequest(); + + spacesService.getActiveSpace.mockRejectedValue(new Error('Something terrible happened')); + + const result = await switcher(request, capabilities); + expect(result).toEqual(buildCapabilities()); + expect(logger.warn).toHaveBeenCalledWith( + `Error toggling capabilities for request to /path: Error: Something terrible happened` + ); }); - it('ignores unknown disabledFeatures', () => { + it('ignores unknown disabledFeatures', async () => { const space: Space = { id: 'space', name: '', @@ -112,11 +184,15 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); + expect(result).toEqual(buildCapabilities()); }); - it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => { + it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', async () => { const space: Space = { id: 'space', name: '', @@ -124,7 +200,10 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); const expectedCapabilities = buildCapabilities(); @@ -137,7 +216,7 @@ describe('toggleUiCapabilities', () => { expect(result).toEqual(expectedCapabilities); }); - it('can disable everything', () => { + it('can disable everything', async () => { const space: Space = { id: 'space', name: '', @@ -145,7 +224,10 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); const expectedCapabilities = buildCapabilities(); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts similarity index 66% rename from x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 2de84ec05017b7..317cc7fe0e3c3c 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -4,15 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -import { UICapabilities } from 'ui/capabilities'; +import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; import { Feature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; +import { SpacesServiceSetup } from '../spaces_service'; +import { PluginsStart } from '../plugin'; -export function toggleUICapabilities( - features: Feature[], - capabilities: UICapabilities, - activeSpace: Space -) { +export function setupCapabilitiesSwitcher( + core: CoreSetup, + spacesService: SpacesServiceSetup, + logger: Logger +): CapabilitiesSwitcher { + return async (request, capabilities) => { + const isAnonymousRequest = !request.route.options.authRequired; + + if (isAnonymousRequest) { + return capabilities; + } + + try { + const [activeSpace, [, { features }]] = await Promise.all([ + spacesService.getActiveSpace(request), + core.getStartServices(), + ]); + + const registeredFeatures = features.getFeatures(); + + return toggleCapabilities(registeredFeatures, capabilities, activeSpace); + } catch (e) { + logger.warn(`Error toggling capabilities for request to ${request.url.pathname}: ${e}`); + return capabilities; + } + }; +} + +function toggleCapabilities(features: Feature[], capabilities: Capabilities, activeSpace: Space) { const clonedCapabilities = _.cloneDeep(capabilities); toggleDisabledFeatures(features, clonedCapabilities, activeSpace); @@ -22,7 +48,7 @@ export function toggleUICapabilities( function toggleDisabledFeatures( features: Feature[], - capabilities: UICapabilities, + capabilities: Capabilities, activeSpace: Space ) { const disabledFeatureKeys = activeSpace.disabledFeatures; diff --git a/x-pack/plugins/spaces/server/capabilities/index.ts b/x-pack/plugins/spaces/server/capabilities/index.ts new file mode 100644 index 00000000000000..56a72a2eeaf197 --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/index.ts @@ -0,0 +1,20 @@ +/* + * 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, Logger } from 'src/core/server'; +import { capabilitiesProvider } from './capabilities_provider'; +import { setupCapabilitiesSwitcher } from './capabilities_switcher'; +import { PluginsStart } from '../plugin'; +import { SpacesServiceSetup } from '../spaces_service'; + +export const setupCapabilities = ( + core: CoreSetup, + spacesService: SpacesServiceSetup, + logger: Logger +) => { + core.capabilities.registerProvider(capabilitiesProvider); + core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger)); +}; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 92be88b91c6523..61157a93187815 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -201,12 +201,10 @@ describe('onPostAuthInterceptor', () => { // interceptor to parse out the space id and rewrite the request's URL. Rather than duplicating that logic, // we are including the already tested interceptor here in the test chain. initSpacesOnRequestInterceptor({ - getLegacyAPI: () => legacyAPI, http: (http as unknown) as CoreSetup['http'], }); initSpacesOnPostAuthRequestInterceptor({ - getLegacyAPI: () => legacyAPI, http: (http as unknown) as CoreSetup['http'], log: loggingMock, features: featuresPlugin, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 4674f3641084ae..b07ff11f6efc62 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -7,13 +7,12 @@ import { Logger, CoreSetup } from 'src/core/server'; import { Space } from '../../../common/model/space'; import { wrapError } from '../errors'; import { SpacesServiceSetup } from '../../spaces_service/spaces_service'; -import { LegacyAPI, PluginsSetup } from '../../plugin'; +import { PluginsSetup } from '../../plugin'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; import { addSpaceIdToPath } from '../../../common'; export interface OnPostAuthInterceptorDeps { - getLegacyAPI(): LegacyAPI; http: CoreSetup['http']; features: PluginsSetup['features']; spacesService: SpacesServiceSetup; @@ -22,7 +21,6 @@ export interface OnPostAuthInterceptorDeps { export function initSpacesOnPostAuthRequestInterceptor({ features, - getLegacyAPI, spacesService, log, http, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 5e6cf67ee8c907..448bc39eb606e0 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -16,7 +16,6 @@ import { } from '../../../../../../src/core/server'; import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; -import { LegacyAPI } from '../../plugin'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('onRequestInterceptor', () => { @@ -110,10 +109,6 @@ describe('onRequestInterceptor', () => { elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; initSpacesOnRequestInterceptor({ - getLegacyAPI: () => - ({ - legacyConfig: {}, - } as LegacyAPI), http: (http as unknown) as CoreSetup['http'], }); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 22d704c1b7e13c..c59851f8b80617 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -12,14 +12,12 @@ import { import { format } from 'url'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { modifyUrl } from '../utils/url'; -import { LegacyAPI } from '../../plugin'; import { getSpaceIdFromPath } from '../../../common'; export interface OnRequestInterceptorDeps { - getLegacyAPI(): LegacyAPI; http: CoreSetup['http']; } -export function initSpacesOnRequestInterceptor({ getLegacyAPI, http }: OnRequestInterceptorDeps) { +export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDeps) { http.registerOnPreAuth(async function spacesOnPreAuthHandler( request: KibanaRequest, response: LifecycleResponseFactory, diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index a3396e98c35122..094ca8a11816ea 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -23,7 +23,6 @@ import { securityMock } from '../../../security/server/mocks'; const log = loggingServiceMock.createLogger(); const legacyAPI: LegacyAPI = { - legacyConfig: {}, savedObjects: {} as SavedObjectsLegacyService, } as LegacyAPI; diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts new file mode 100644 index 00000000000000..4e3f4f52cbeb49 --- /dev/null +++ b/x-pack/plugins/spaces/server/plugin.test.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 { CoreSetup } from 'src/core/server'; +import { coreMock } from 'src/core/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; +import { licensingMock } from '../../licensing/server/mocks'; +import { Plugin, PluginsSetup } from './plugin'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; + +describe('Spaces Plugin', () => { + describe('#setup', () => { + it('can setup with all optional plugins disabled, exposing the expected contract', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const plugin = new Plugin(initializerContext); + const spacesSetup = await plugin.setup(core, { features, licensing }); + expect(spacesSetup).toMatchInlineSnapshot(` + Object { + "__legacyCompat": Object { + "createDefaultSpace": [Function], + "registerLegacyAPI": [Function], + }, + "spacesService": Object { + "getActiveSpace": [Function], + "getBasePath": [Function], + "getSpaceId": [Function], + "isInDefaultSpace": [Function], + "namespaceToSpaceId": [Function], + "scopedClient": [Function], + "spaceIdToNamespace": [Function], + }, + } + `); + }); + + it('registers the capabilities provider and switcher', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const plugin = new Plugin(initializerContext); + + await plugin.setup(core, { features, licensing }); + + expect(core.capabilities.registerProvider).toHaveBeenCalledTimes(1); + expect(core.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + }); + + it('registers the usage collector', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const usageCollection = usageCollectionPluginMock.createSetupContract(); + + const plugin = new Plugin(initializerContext); + + await plugin.setup(core, { features, licensing, usageCollection }); + + expect(usageCollection.getCollectorByType('spaces')).toBeDefined(); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 90c2da6e69df8c..d125e0f54e9c1f 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,7 +13,10 @@ import { Logger, PluginInitializerContext, } from '../../../../src/core/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { createDefaultSpace } from './lib/create_default_space'; @@ -22,15 +25,15 @@ 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 { registerSpacesUsageCollector } from './lib/spaces_usage_collector'; +import { registerSpacesUsageCollector } from './usage_collection'; import { SpacesService } from './spaces_service'; import { SpacesServiceSetup } from './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 { initInternalSpacesApi } from './routes/api/internal'; import { initSpacesViewsRoutes } from './routes/views'; +import { setupCapabilities } from './capabilities'; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin @@ -41,9 +44,6 @@ export interface LegacyAPI { auditLogger: { create: (pluginId: string) => AuditLogger; }; - legacyConfig: { - kibanaIndex: string; - }; } export interface PluginsSetup { @@ -54,6 +54,10 @@ export interface PluginsSetup { home?: HomeServerPluginSetup; } +export interface PluginsStart { + features: FeaturesPluginStart; +} + export interface SpacesPluginSetup { spacesService: SpacesServiceSetup; __legacyCompat: { @@ -70,6 +74,8 @@ export class Plugin { private readonly config$: Observable; + private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; + private readonly log: Logger; private legacyAPI?: LegacyAPI; @@ -92,12 +98,16 @@ export class Plugin { constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); + this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$; this.log = initializerContext.logger.get(); } public async start() {} - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: PluginsSetup + ): Promise { const service = new SpacesService(this.log, this.getLegacyAPI); const spacesService = await service.setup({ @@ -131,20 +141,19 @@ export class Plugin { initSpacesRequestInterceptors({ http: core.http, log: this.log, - getLegacyAPI: this.getLegacyAPI, spacesService, features: plugins.features, }); - core.capabilities.registerSwitcher(async (request, uiCapabilities) => { - try { - const activeSpace = await spacesService.getActiveSpace(request); - const features = plugins.features.getFeatures(); - return toggleUICapabilities(features, uiCapabilities, activeSpace); - } catch (e) { - return uiCapabilities; - } - }); + setupCapabilities(core, spacesService, this.log); + + if (plugins.usageCollection) { + registerSpacesUsageCollector(plugins.usageCollection, { + kibanaIndexConfig$: this.kibanaIndexConfig$, + features: plugins.features, + licensing: plugins.licensing, + }); + } if (plugins.security) { plugins.security.registerSpacesService(spacesService); @@ -161,12 +170,7 @@ export class Plugin { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; - this.setupLegacyComponents( - spacesService, - plugins.features, - plugins.licensing, - plugins.usageCollection - ); + this.setupLegacyComponents(spacesService); }, createDefaultSpace: async () => { return await createDefaultSpace({ @@ -180,12 +184,7 @@ export class Plugin { public stop() {} - private setupLegacyComponents( - spacesService: SpacesServiceSetup, - featuresSetup: FeaturesPluginSetup, - licensingSetup: LicensingPluginSetup, - usageCollectionSetup?: UsageCollectionSetup - ) { + private setupLegacyComponents(spacesService: SpacesServiceSetup) { const legacyAPI = this.getLegacyAPI(); const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects; addScopedSavedObjectsClientWrapperFactory( @@ -193,11 +192,5 @@ export class Plugin { 'spaces', spacesSavedObjectsClientWrapperFactory(spacesService, types) ); - // Register a function with server to manage the collection of usage stats - registerSpacesUsageCollector(usageCollectionSetup, { - kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, - 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 812b02e94f591b..7765cc3c52e966 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 @@ -100,9 +100,6 @@ export const createLegacyAPI = ({ } as unknown) as jest.Mocked; const legacyAPI: jest.Mocked = { - legacyConfig: { - kibanaIndex: '', - }, auditLogger: {} as any, savedObjects: savedObjectsService, }; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index 68d096e046ed4f..fc5ff397805244 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -28,7 +28,6 @@ const mockLogger = loggingServiceMock.createLogger(); const createService = async (serverBasePath: string = '') => { const legacyAPI = { - legacyConfig: {}, savedObjects: ({ getSavedObjectsRepository: jest.fn().mockReturnValue({ get: jest.fn().mockImplementation((type, id) => { diff --git a/x-pack/plugins/spaces/server/usage_collection/index.ts b/x-pack/plugins/spaces/server/usage_collection/index.ts new file mode 100644 index 00000000000000..01df2b815f5ff3 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_collection/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 { registerSpacesUsageCollector } from './spaces_usage_collector'; diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts similarity index 85% rename from x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts rename to x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index c0a6a152c8322b..57ec688ab70e83 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -9,6 +9,7 @@ import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; import { Feature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; +import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; interface SetupOpts { license?: Partial; @@ -72,7 +73,7 @@ describe('error handling', () => { license: { isAvailable: true, type: 'basic' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, }); @@ -85,7 +86,7 @@ describe('error handling', () => { license: { isAvailable: true, type: 'basic' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, }); @@ -105,11 +106,25 @@ describe('with a basic license', () => { license: { isAvailable: true, type: 'basic' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); usageStats = await getSpacesUsage(defaultCallClusterMock); + + expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { + body: { + aggs: { + disabledFeatures: { + terms: { field: 'space.disabledFeatures', include: ['feature1', 'feature2'], size: 2 }, + }, + }, + query: { term: { type: { value: 'space' } } }, + size: 0, + track_total_hits: true, + }, + index: '.kibana-tests', + }); }); test('sets enabled to true', () => { @@ -139,7 +154,7 @@ describe('with no license', () => { beforeAll(async () => { const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); @@ -170,7 +185,7 @@ describe('with platinum license', () => { license: { isAvailable: true, type: 'platinum' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts similarity index 90% rename from x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts rename to x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index af77f2d3a72bac..90187b7853185a 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -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 { Observable } from 'rxjs'; import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; import { PluginsSetup } from '../plugin'; @@ -85,8 +84,8 @@ async function getSpacesUsage( const { hits, aggregations } = resp!; - const count = get(hits, 'total.value', 0); - const disabledFeatureBuckets = get(aggregations, 'disabledFeatures.buckets', []); + const count = hits?.total?.value ?? 0; + const disabledFeatureBuckets = aggregations?.disabledFeatures?.buckets ?? []; const initialCounts = knownFeatureIds.reduce( (acc, featureId) => ({ ...acc, [featureId]: 0 }), @@ -125,7 +124,7 @@ export interface UsageStats { } interface CollectorDeps { - kibanaIndex: string; + kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; features: PluginsSetup['features']; licensing: PluginsSetup['licensing']; } @@ -145,12 +144,9 @@ export function getSpacesUsageCollector( const license = await deps.licensing.license$.pipe(take(1)).toPromise(); const available = license.isAvailable; // some form of spaces is available for all valid licenses - const usageStats = await getSpacesUsage( - callCluster, - deps.kibanaIndex, - deps.features, - available - ); + const kibanaIndex = (await deps.kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index; + + const usageStats = await getSpacesUsage(callCluster, kibanaIndex, deps.features, available); return { available, @@ -178,12 +174,9 @@ export function getSpacesUsageCollector( } export function registerSpacesUsageCollector( - usageCollection: UsageCollectionSetup | undefined, + usageCollection: UsageCollectionSetup, deps: CollectorDeps ) { - if (!usageCollection) { - return; - } const collector = getSpacesUsageCollector(usageCollection, deps); usageCollection.registerCollector(collector); } From 38067da7acd6c77947eafcfe04d894037c5c3351 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 28 Feb 2020 14:30:04 +0000 Subject: [PATCH 34/34] [ML] Fixing annotations alias checks (#58722) --- .../ml/server/lib/check_annotations/index.d.ts | 11 ----------- .../check_annotations/{index.js => index.ts} | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 x-pack/plugins/ml/server/lib/check_annotations/index.d.ts rename x-pack/plugins/ml/server/lib/check_annotations/{index.js => index.ts} (79%) diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.d.ts b/x-pack/plugins/ml/server/lib/check_annotations/index.d.ts deleted file mode 100644 index dbd08eacd3ca2e..00000000000000 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.d.ts +++ /dev/null @@ -1,11 +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 { IScopedClusterClient } from 'src/core/server'; - -export function isAnnotationsFeatureAvailable( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] -): boolean; diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.js b/x-pack/plugins/ml/server/lib/check_annotations/index.ts similarity index 79% rename from x-pack/plugins/ml/server/lib/check_annotations/index.js rename to x-pack/plugins/ml/server/lib/check_annotations/index.ts index 55a90c0cec322c..8d9d56ad665c43 100644 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.js +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'src/core/server'; import { mlLog } from '../../client/log'; import { @@ -16,23 +17,31 @@ import { // - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present -export async function isAnnotationsFeatureAvailable(callAsCurrentUser) { +export async function isAnnotationsFeatureAvailable(callAsCurrentUser: APICaller) { try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; const annotationsIndexExists = await callAsCurrentUser('indices.exists', indexParams); - if (!annotationsIndexExists) return false; + if (!annotationsIndexExists) { + return false; + } const annotationsReadAliasExists = await callAsCurrentUser('indices.existsAlias', { + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, name: ML_ANNOTATIONS_INDEX_ALIAS_READ, }); - if (!annotationsReadAliasExists) return false; + if (!annotationsReadAliasExists) { + return false; + } const annotationsWriteAliasExists = await callAsCurrentUser('indices.existsAlias', { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, name: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, }); - if (!annotationsWriteAliasExists) return false; + if (!annotationsWriteAliasExists) { + return false; + } } catch (err) { mlLog.info('Disabling ML annotations feature because the index/alias integrity check failed.'); return false;

+ + ); + + expect(props.registerProvider.mock.calls.length).toEqual(0); + }); + + it('calls registerProvider when isDragging', () => { + mount( + +
+ + ); + + expect(props.registerProvider.mock.calls.length).toEqual(1); + }); +}); 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 7d84403b87f8d0..4b80b9fff2740e 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 @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useContext, useEffect } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { Draggable, DraggableProvided, DraggableStateSnapshot, Droppable, } from 'react-beautiful-dnd'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -47,34 +47,50 @@ const ProviderContentWrapper = styled.span` } `; +type RenderFunctionProp = ( + props: DataProvider, + provided: DraggableProvided, + state: DraggableStateSnapshot +) => React.ReactNode; + interface OwnProps { dataProvider: DataProvider; inline?: boolean; - render: ( - props: DataProvider, - provided: DraggableProvided, - state: DraggableStateSnapshot - ) => React.ReactNode; + render: RenderFunctionProp; truncate?: boolean; } -type Props = OwnProps & PropsFromRedux; +type Props = OwnProps; /** * Wraps a draggable component to handle registration / unregistration of the * data provider associated with the item being dropped */ -const DraggableWrapperComponent = React.memo( - ({ dataProvider, registerProvider, render, truncate, unRegisterProvider }) => { +export const DraggableWrapper = React.memo( + ({ dataProvider, render, truncate }) => { + const [providerRegistered, setProviderRegistered] = useState(false); + const dispatch = useDispatch(); const usePortal = useDraggablePortalContext(); - useEffect(() => { - registerProvider!({ provider: dataProvider }); - return () => { - unRegisterProvider!({ id: dataProvider.id }); - }; - }, []); + const registerProvider = useCallback(() => { + if (!providerRegistered) { + dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); + setProviderRegistered(true); + } + }, [dispatch, providerRegistered, dataProvider]); + + const unRegisterProvider = useCallback( + () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), + [dispatch, dataProvider] + ); + + useEffect( + () => () => { + unRegisterProvider(); + }, + [] + ); return ( @@ -87,13 +103,18 @@ const DraggableWrapperComponent = React.memo( key={getDraggableId(dataProvider.id)} > {(provided, snapshot) => ( - + ( ); }, - (prevProps, nextProps) => { - return ( - deepEqual(prevProps.dataProvider, nextProps.dataProvider) && - prevProps.render !== nextProps.render && - prevProps.truncate === nextProps.truncate - ); - } + (prevProps, nextProps) => + deepEqual(prevProps.dataProvider, nextProps.dataProvider) && + prevProps.render !== nextProps.render && + prevProps.truncate === nextProps.truncate ); -DraggableWrapperComponent.displayName = 'DraggableWrapperComponent'; - -const mapDispatchToProps = { - registerProvider: dragAndDropActions.registerProvider, - unRegisterProvider: dragAndDropActions.unRegisterProvider, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const DraggableWrapper = connector(DraggableWrapperComponent); - DraggableWrapper.displayName = 'DraggableWrapper'; /** @@ -150,8 +155,24 @@ DraggableWrapper.displayName = 'DraggableWrapper'; * * See: https://github.com/atlassian/react-beautiful-dnd/issues/499 */ -const ConditionalPortal = React.memo<{ children: React.ReactNode; usePortal: boolean }>( - ({ children, usePortal }) => (usePortal ? {children} : <>{children}) + +interface ConditionalPortalProps { + children: React.ReactNode; + usePortal: boolean; + isDragging: boolean; + registerProvider: () => void; +} + +export const ConditionalPortal = React.memo( + ({ children, usePortal, registerProvider, isDragging }) => { + useEffect(() => { + if (isDragging) { + registerProvider(); + } + }, [isDragging, registerProvider]); + + return usePortal ? {children} : <>{children}; + } ); ConditionalPortal.displayName = 'ConditionalPortal'; From d474ccf244d22b8abf7df1be3e96b36b715281f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 27 Feb 2020 22:55:02 +0100 Subject: [PATCH 15/34] [APM] Fix timeout in APM setup (#58727) * [APM] Fix timeout in APM setup * Update plugin.ts --- src/plugins/apm_oss/server/index.ts | 2 +- src/plugins/apm_oss/server/plugin.ts | 6 ++- .../get_dynamic_index_pattern.ts | 3 +- .../create_agent_config_index.ts | 9 ++-- x-pack/plugins/apm/server/plugin.ts | 43 ++++++++----------- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index 801140694c1394..95a4ae4519bc9a 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -38,4 +38,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export type APMOSSConfig = TypeOf; -export { APMOSSPlugin as Plugin }; +export { APMOSSPluginSetup } from './plugin'; diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index 2708f7729482bd..9b14d19da90c2e 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -20,7 +20,7 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; import { APMOSSConfig } from './'; -export class APMOSSPlugin implements Plugin<{ config$: Observable }> { +export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } @@ -36,3 +36,7 @@ export class APMOSSPlugin implements Plugin<{ config$: Observable start() {} stop() {} } + +export interface APMOSSPluginSetup { + config$: Observable; +} diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index b1e4906317f815..a7476bf564a16b 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -62,8 +62,7 @@ export const getDynamicIndexPattern = async ({ cache.set(CACHE_KEY, undefined); const notExists = e.output?.statusCode === 404; if (notExists) { - // eslint-disable-next-line no-console - console.error( + context.logger.error( `Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist` ); return; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index af2d2a13eaa2fa..8cfb7e7edb4c69 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IClusterClient } from 'src/core/server'; +import { IClusterClient, Logger } from 'src/core/server'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export async function createApmAgentConfigurationIndex({ esClient, - config + config, + logger }: { esClient: IClusterClient; config: APMConfig; + logger: Logger; }) { try { const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; @@ -32,8 +34,7 @@ export async function createApmAgentConfigurationIndex({ ); } } catch (e) { - // eslint-disable-next-line no-console - console.error('Could not create APM Agent configuration:', e.message); + logger.error(`Could not create APM Agent configuration: ${e.message}`); } } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index adc80cb43620bf..773f0d4e6fac56 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -5,12 +5,12 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; import { Observable, combineLatest, AsyncSubject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { Server } from 'hapi'; import { once } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; import { makeApmUsageCollector } from './lib/apm_telemetry'; -import { Plugin as APMOSSPlugin } from '../../../../src/plugins/apm_oss/server'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { createApmApi } from './routes/create_apm_api'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; @@ -33,26 +33,23 @@ export interface APMPluginContract { export class APMPlugin implements Plugin { legacySetup$: AsyncSubject; - currentConfig: APMConfig; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.legacySetup$ = new AsyncSubject(); - this.currentConfig = {} as APMConfig; } public async setup( core: CoreSetup, plugins: { - apm_oss: APMOSSPlugin extends Plugin ? TSetup : never; + apm_oss: APMOSSPluginSetup; home: HomeServerPluginSetup; licensing: LicensingPluginSetup; cloud?: CloudSetup; usageCollection?: UsageCollectionSetup; } ) { - const config$ = this.initContext.config.create(); const logger = this.initContext.logger.get('apm'); - + const config$ = this.initContext.config.create(); const mergedConfig$ = combineLatest(plugins.apm_oss.config$, config$).pipe( map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) ); @@ -61,28 +58,26 @@ export class APMPlugin implements Plugin { createApmApi().init(core, { config$: mergedConfig$, logger, __LEGACY }); }); - await new Promise(resolve => { - mergedConfig$.subscribe(async config => { - this.currentConfig = config; - await createApmAgentConfigurationIndex({ - esClient: core.elasticsearch.dataClient, - config - }); - resolve(); - }); + const currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + + // create agent configuration index without blocking setup lifecycle + createApmAgentConfigurationIndex({ + esClient: core.elasticsearch.dataClient, + config: currentConfig, + logger }); plugins.home.tutorials.registerTutorial( tutorialProvider({ - isEnabled: this.currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], + isEnabled: currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: currentConfig['apm_oss.indexPattern'], cloud: plugins.cloud, indices: { - errorIndices: this.currentConfig['apm_oss.errorIndices'], - metricsIndices: this.currentConfig['apm_oss.metricsIndices'], - onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: this.currentConfig['apm_oss.transactionIndices'] + errorIndices: currentConfig['apm_oss.errorIndices'], + metricsIndices: currentConfig['apm_oss.metricsIndices'], + onboardingIndices: currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: currentConfig['apm_oss.transactionIndices'] } }) ); @@ -108,7 +103,7 @@ export class APMPlugin implements Plugin { getApmIndices: async () => getApmIndices({ savedObjectsClient: await getInternalSavedObjectsClient(core), - config: this.currentConfig + config: currentConfig }) }; } From 07c22f13b73ee29ade817d8abb8f73823df76899 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 27 Feb 2020 15:23:54 -0700 Subject: [PATCH 16/34] skip flaky suite (#58785) --- x-pack/test/api_integration/apis/security/privileges.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 7b1984222404bc..81cffaac07285b 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -9,7 +9,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Privileges', () => { + // FLAKY: https://github.com/elastic/kibana/issues/58785 + describe.skip('Privileges', () => { describe('GET /api/security/privileges', () => { it('should return a privilege map with all known privileges, without actions', async () => { await supertest From 467280232abf822d258aef5459e91e0375124485 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 27 Feb 2020 17:19:35 -0700 Subject: [PATCH 17/34] Revert "[SIEM] Fix Timeline registerProvider to be called only when it's needed (#58051)" This reverts commit 2a03dffdad6c1dbeaf9a541c4ea0fb84183a8ca8. --- .../drag_and_drop/draggable_wrapper.test.tsx | 31 +----- .../drag_and_drop/draggable_wrapper.tsx | 99 ++++++++----------- 2 files changed, 40 insertions(+), 90 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index d34f4cce9fea41..92adc1a9adb7a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -12,7 +12,7 @@ import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; -import { DraggableWrapper, ConditionalPortal } from './draggable_wrapper'; +import { DraggableWrapper } from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; describe('DraggableWrapper', () => { @@ -84,32 +84,3 @@ describe('DraggableWrapper', () => { }); }); }); - -describe('ConditionalPortal', () => { - const mount = useMountAppended(); - const props = { - usePortal: false, - registerProvider: jest.fn(), - isDragging: true, - }; - - it(`doesn't call registerProvider is NOT isDragging`, () => { - mount( - -
- - ); - - expect(props.registerProvider.mock.calls.length).toEqual(0); - }); - - it('calls registerProvider when isDragging', () => { - mount( - -
- - ); - - expect(props.registerProvider.mock.calls.length).toEqual(1); - }); -}); 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 4b80b9fff2740e..7d84403b87f8d0 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 @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useContext, useEffect } from 'react'; import { Draggable, DraggableProvided, DraggableStateSnapshot, Droppable, } from 'react-beautiful-dnd'; -import { useDispatch } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -47,50 +47,34 @@ const ProviderContentWrapper = styled.span` } `; -type RenderFunctionProp = ( - props: DataProvider, - provided: DraggableProvided, - state: DraggableStateSnapshot -) => React.ReactNode; - interface OwnProps { dataProvider: DataProvider; inline?: boolean; - render: RenderFunctionProp; + render: ( + props: DataProvider, + provided: DraggableProvided, + state: DraggableStateSnapshot + ) => React.ReactNode; truncate?: boolean; } -type Props = OwnProps; +type Props = OwnProps & PropsFromRedux; /** * Wraps a draggable component to handle registration / unregistration of the * data provider associated with the item being dropped */ -export const DraggableWrapper = React.memo( - ({ dataProvider, render, truncate }) => { - const [providerRegistered, setProviderRegistered] = useState(false); - const dispatch = useDispatch(); +const DraggableWrapperComponent = React.memo( + ({ dataProvider, registerProvider, render, truncate, unRegisterProvider }) => { const usePortal = useDraggablePortalContext(); - const registerProvider = useCallback(() => { - if (!providerRegistered) { - dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); - setProviderRegistered(true); - } - }, [dispatch, providerRegistered, dataProvider]); - - const unRegisterProvider = useCallback( - () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), - [dispatch, dataProvider] - ); - - useEffect( - () => () => { - unRegisterProvider(); - }, - [] - ); + useEffect(() => { + registerProvider!({ provider: dataProvider }); + return () => { + unRegisterProvider!({ id: dataProvider.id }); + }; + }, []); return ( @@ -103,18 +87,13 @@ export const DraggableWrapper = React.memo( key={getDraggableId(dataProvider.id)} > {(provided, snapshot) => ( - + ( ); }, - (prevProps, nextProps) => - deepEqual(prevProps.dataProvider, nextProps.dataProvider) && - prevProps.render !== nextProps.render && - prevProps.truncate === nextProps.truncate + (prevProps, nextProps) => { + return ( + deepEqual(prevProps.dataProvider, nextProps.dataProvider) && + prevProps.render !== nextProps.render && + prevProps.truncate === nextProps.truncate + ); + } ); +DraggableWrapperComponent.displayName = 'DraggableWrapperComponent'; + +const mapDispatchToProps = { + registerProvider: dragAndDropActions.registerProvider, + unRegisterProvider: dragAndDropActions.unRegisterProvider, +}; + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const DraggableWrapper = connector(DraggableWrapperComponent); + DraggableWrapper.displayName = 'DraggableWrapper'; /** @@ -155,24 +150,8 @@ DraggableWrapper.displayName = 'DraggableWrapper'; * * See: https://github.com/atlassian/react-beautiful-dnd/issues/499 */ - -interface ConditionalPortalProps { - children: React.ReactNode; - usePortal: boolean; - isDragging: boolean; - registerProvider: () => void; -} - -export const ConditionalPortal = React.memo( - ({ children, usePortal, registerProvider, isDragging }) => { - useEffect(() => { - if (isDragging) { - registerProvider(); - } - }, [isDragging, registerProvider]); - - return usePortal ? {children} : <>{children}; - } +const ConditionalPortal = React.memo<{ children: React.ReactNode; usePortal: boolean }>( + ({ children, usePortal }) => (usePortal ? {children} : <>{children}) ); ConditionalPortal.displayName = 'ConditionalPortal'; From facae44f5b17481cf450e5b310b33a16239feff0 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 27 Feb 2020 18:03:04 -0700 Subject: [PATCH 18/34] skip flaky suite (#53888) --- test/functional/apps/context/_size.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.js index 5f3d1ebe409744..ea9b2c8cf18192 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.js @@ -30,7 +30,8 @@ export default function({ getService, getPageObjects }) { const docTable = getService('docTable'); const PageObjects = getPageObjects(['context']); - describe('context size', function contextSize() { + // FLAKY: https://github.com/elastic/kibana/issues/53888 + describe.skip('context size', function contextSize() { before(async function() { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, From 8c3d71b370fadf5872185decedc34886ae151263 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 27 Feb 2020 21:15:08 -0500 Subject: [PATCH 19/34] [ML] NP: migrate server (#58680) * remove obsolete legacy server deps * licensePreRoutingFactory uses licensing plugin rather than legacy xpack * move schemas to dir in routes * use NP license check method for license check * store license data in plugin for passing to check * create server plugin files in NP plugin dir * remove dependency on legacy xpack plugin * add sample data links first step * move all server dirs from legacy to np dir * fix requiredPlugin spaces name and update import routes * delete unnecessary files and add sample data links * update license and privilege check tests * add routeInit types --- x-pack/.i18nrc.json | 2 +- .../legacy/plugins/ml/common/constants/app.ts | 2 +- .../plugins/ml/common/constants/license.ts | 2 + x-pack/legacy/plugins/ml/index.ts | 55 +--- x-pack/legacy/plugins/ml/kibana.json | 8 - .../application/license/check_license.tsx | 11 +- .../call_with_internal_user_factory.d.ts | 9 - .../client/call_with_internal_user_factory.js | 18 -- .../call_with_internal_user_factory.test.ts | 28 --- .../client/call_with_request_factory.js | 21 -- .../ml/server/lib/__tests__/security_utils.js | 35 --- .../plugins/ml/server/lib/security_utils.d.ts | 9 - .../plugins/ml/server/lib/security_utils.js | 19 -- .../plugins/ml/server/new_platform/plugin.ts | 238 ------------------ x-pack/plugins/ml/kibana.json | 9 + .../client/__tests__/elasticsearch_ml.js | 0 .../ml/server/client/elasticsearch_ml.js | 0 .../plugins/ml/server/client/error_wrapper.ts | 0 .../plugins/ml/server/client/errors.js | 0 .../plugins/ml/server/client/log.ts | 2 +- .../ml/server}/index.ts | 7 +- .../ml/server/lib/__tests__/query_utils.js | 0 .../server/lib/check_annotations/index.d.ts | 0 .../ml/server/lib/check_annotations/index.js | 2 +- .../lib/check_license/check_license.test.ts | 46 ++-- .../server/lib/check_license/check_license.ts | 22 +- .../ml/server/lib/check_license/index.ts | 0 .../__mocks__/call_with_request.ts | 0 .../check_privileges/check_privileges.test.ts | 120 +++------ .../lib/check_privileges/check_privileges.ts | 16 +- .../ml/server/lib/check_privileges/index.ts | 0 .../server/lib/check_privileges/privileges.ts | 0 .../ml/server/lib/check_privileges/upgrade.ts | 0 .../ml/server/lib/ml_telemetry/index.ts | 0 .../ml_telemetry/make_ml_usage_collector.ts | 0 .../lib/ml_telemetry/ml_telemetry.test.ts | 30 +-- .../server/lib/ml_telemetry/ml_telemetry.ts | 19 +- .../plugins/ml/server/lib/query_utils.ts | 0 .../ml/server/lib/sample_data_sets/index.ts | 0 .../lib/sample_data_sets/sample_data_sets.ts | 0 .../plugins/ml/server/lib/spaces_utils.ts | 7 +- .../__mocks__/get_annotations_request.json | 0 .../__mocks__/get_annotations_response.json | 0 .../annotation_service/annotation.test.ts | 9 +- .../models/annotation_service/annotation.ts | 6 +- .../server/models/annotation_service/index.ts | 0 .../__tests__/bucket_span_estimator.js | 0 .../bucket_span_estimator.d.ts | 4 +- .../bucket_span_estimator.js | 10 +- .../models/bucket_span_estimator/index.ts | 0 .../models/bucket_span_estimator/intervals.js | 0 .../polled_data_checker.js | 0 .../single_series_checker.js | 0 .../calculate_model_memory_limit.d.ts | 0 .../calculate_model_memory_limit.js | 0 .../calculate_model_memory_limit/index.ts | 0 .../models/calendar/calendar_manager.ts | 0 .../server/models/calendar/event_manager.ts | 2 +- .../ml/server/models/calendar/index.ts | 0 .../analytics_audit_messages.ts | 6 +- .../models/data_frame_analytics/index.js | 0 .../data_recognizer/data_recognizer.test.ts | 2 +- .../models/data_recognizer/data_recognizer.ts | 9 +- .../ml/server/models/data_recognizer/index.ts | 0 .../ml_http_access_explorer_ecs.json | 0 .../search/ml_http_access_filebeat_ecs.json | 0 .../ml_http_access_events_timechart_ecs.json | 0 .../visualization/ml_http_access_map_ecs.json | 0 ...l_http_access_source_ip_timechart_ecs.json | 0 ...http_access_status_code_timechart_ecs.json | 0 ..._http_access_top_source_ips_table_ecs.json | 0 .../ml_http_access_top_urls_table_ecs.json | 0 ...access_unique_count_url_timechart_ecs.json | 0 .../modules/apache_ecs/logo.json | 0 .../modules/apache_ecs/manifest.json | 0 .../ml/datafeed_low_request_rate_ecs.json | 0 .../datafeed_source_ip_request_rate_ecs.json | 0 .../ml/datafeed_source_ip_url_count_ecs.json | 0 .../ml/datafeed_status_code_rate_ecs.json | 0 .../ml/datafeed_visitor_rate_ecs.json | 0 .../apache_ecs/ml/low_request_rate_ecs.json | 0 .../ml/source_ip_request_rate_ecs.json | 0 .../ml/source_ip_url_count_ecs.json | 0 .../apache_ecs/ml/status_code_rate_ecs.json | 0 .../apache_ecs/ml/visitor_rate_ecs.json | 0 .../modules/apm_jsbase/logo.json | 0 .../modules/apm_jsbase/manifest.json | 0 .../ml/abnormal_span_durations_jsbase.json | 0 ...ous_error_rate_for_user_agents_jsbase.json | 0 ...tafeed_abnormal_span_durations_jsbase.json | 0 ...ous_error_rate_for_user_agents_jsbase.json | 0 .../datafeed_decreased_throughput_jsbase.json | 0 ...afeed_high_count_by_user_agent_jsbase.json | 0 .../ml/decreased_throughput_jsbase.json | 0 .../ml/high_count_by_user_agent_jsbase.json | 0 .../modules/apm_nodejs/logo.json | 0 .../modules/apm_nodejs/manifest.json | 0 .../ml/abnormal_span_durations_nodejs.json | 0 .../ml/abnormal_trace_durations_nodejs.json | 0 ...tafeed_abnormal_span_durations_nodejs.json | 0 ...afeed_abnormal_trace_durations_nodejs.json | 0 .../datafeed_decreased_throughput_nodejs.json | 0 .../ml/decreased_throughput_nodejs.json | 0 .../modules/apm_transaction/logo.json | 0 .../modules/apm_transaction/manifest.json | 0 .../ml/datafeed_high_mean_response_time.json | 0 .../ml/high_mean_response_time.json | 0 ...ditbeat_docker_process_event_rate_ecs.json | 0 ...auditbeat_docker_process_explorer_ecs.json | 0 ...l_auditbeat_docker_process_events_ecs.json | 0 ...ker_process_event_rate_by_process_ecs.json | 0 ...eat_docker_process_event_rate_vis_ecs.json | 0 ...ditbeat_docker_process_occurrence_ecs.json | 0 .../auditbeat_process_docker_ecs/logo.json | 0 .../manifest.json | 0 ..._docker_high_count_process_events_ecs.json | 0 ...feed_docker_rare_process_activity_ecs.json | 0 .../docker_high_count_process_events_ecs.json | 0 .../ml/docker_rare_process_activity_ecs.json | 0 ...uditbeat_hosts_process_event_rate_ecs.json | 0 ..._auditbeat_hosts_process_explorer_ecs.json | 0 ...ml_auditbeat_hosts_process_events_ecs.json | 0 ...sts_process_event_rate_by_process_ecs.json | 0 ...beat_hosts_process_event_rate_vis_ecs.json | 0 ...uditbeat_hosts_process_occurrence_ecs.json | 0 .../auditbeat_process_hosts_ecs/logo.json | 0 .../auditbeat_process_hosts_ecs/manifest.json | 0 ...d_hosts_high_count_process_events_ecs.json | 0 ...afeed_hosts_rare_process_activity_ecs.json | 0 .../hosts_high_count_process_events_ecs.json | 0 .../ml/hosts_rare_process_activity_ecs.json | 0 .../modules/logs_ui_analysis/logo.json | 0 .../modules/logs_ui_analysis/manifest.json | 0 .../ml/datafeed_log_entry_rate.json | 0 .../logs_ui_analysis/ml/log_entry_rate.json | 0 .../modules/logs_ui_categories/logo.json | 0 .../modules/logs_ui_categories/manifest.json | 0 .../datafeed_log_entry_categories_count.json | 0 .../ml/log_entry_categories_count.json | 0 .../modules/metricbeat_system_ecs/logo.json | 0 .../metricbeat_system_ecs/manifest.json | 0 .../ml/datafeed_high_mean_cpu_iowait_ecs.json | 0 .../ml/datafeed_max_disk_utilization_ecs.json | 0 .../ml/datafeed_metricbeat_outages_ecs.json | 0 .../ml/high_mean_cpu_iowait_ecs.json | 0 .../ml/max_disk_utilization_ecs.json | 0 .../ml/metricbeat_outages_ecs.json | 0 .../ml_http_access_explorer_ecs.json | 0 .../search/ml_http_access_filebeat_ecs.json | 0 .../ml_http_access_events_timechart_ecs.json | 0 .../visualization/ml_http_access_map_ecs.json | 0 ...l_http_access_source_ip_timechart_ecs.json | 0 ...http_access_status_code_timechart_ecs.json | 0 ..._http_access_top_source_ips_table_ecs.json | 0 .../ml_http_access_top_urls_table_ecs.json | 0 ...access_unique_count_url_timechart_ecs.json | 0 .../modules/nginx_ecs/logo.json | 0 .../modules/nginx_ecs/manifest.json | 0 .../ml/datafeed_low_request_rate_ecs.json | 0 .../datafeed_source_ip_request_rate_ecs.json | 0 .../ml/datafeed_source_ip_url_count_ecs.json | 0 .../ml/datafeed_status_code_rate_ecs.json | 0 .../ml/datafeed_visitor_rate_ecs.json | 0 .../nginx_ecs/ml/low_request_rate_ecs.json | 0 .../ml/source_ip_request_rate_ecs.json | 0 .../nginx_ecs/ml/source_ip_url_count_ecs.json | 0 .../nginx_ecs/ml/status_code_rate_ecs.json | 0 .../nginx_ecs/ml/visitor_rate_ecs.json | 0 .../modules/sample_data_ecommerce/logo.json | 0 .../sample_data_ecommerce/manifest.json | 0 .../ml/datafeed_high_sum_total_sales.json | 0 .../ml/high_sum_total_sales.json | 0 .../modules/sample_data_weblogs/logo.json | 0 .../modules/sample_data_weblogs/manifest.json | 0 .../ml/datafeed_low_request_rate.json | 0 .../ml/datafeed_response_code_rates.json | 0 .../ml/datafeed_url_scanning.json | 0 .../ml/low_request_rate.json | 0 .../ml/response_code_rates.json | 0 .../sample_data_weblogs/ml/url_scanning.json | 0 .../modules/siem_auditbeat/logo.json | 0 .../modules/siem_auditbeat/manifest.json | 0 ..._linux_anomalous_network_activity_ecs.json | 0 ...x_anomalous_network_port_activity_ecs.json | 0 ...afeed_linux_anomalous_network_service.json | 0 ...ux_anomalous_network_url_activity_ecs.json | 0 ...linux_anomalous_process_all_hosts_ecs.json | 0 ...atafeed_linux_anomalous_user_name_ecs.json | 0 ...tafeed_rare_process_by_host_linux_ecs.json | 0 .../linux_anomalous_network_activity_ecs.json | 0 ...x_anomalous_network_port_activity_ecs.json | 0 .../ml/linux_anomalous_network_service.json | 0 ...ux_anomalous_network_url_activity_ecs.json | 0 ...linux_anomalous_process_all_hosts_ecs.json | 0 .../ml/linux_anomalous_user_name_ecs.json | 0 .../ml/rare_process_by_host_linux_ecs.json | 0 .../modules/siem_auditbeat_auth/logo.json | 0 .../modules/siem_auditbeat_auth/manifest.json | 0 ...atafeed_suspicious_login_activity_ecs.json | 0 .../ml/suspicious_login_activity_ecs.json | 0 .../modules/siem_packetbeat/logo.json | 0 .../modules/siem_packetbeat/manifest.json | 0 .../ml/datafeed_packetbeat_dns_tunneling.json | 0 ...datafeed_packetbeat_rare_dns_question.json | 0 ...atafeed_packetbeat_rare_server_domain.json | 0 .../ml/datafeed_packetbeat_rare_urls.json | 0 .../datafeed_packetbeat_rare_user_agent.json | 0 .../ml/packetbeat_dns_tunneling.json | 0 .../ml/packetbeat_rare_dns_question.json | 0 .../ml/packetbeat_rare_server_domain.json | 0 .../ml/packetbeat_rare_urls.json | 0 .../ml/packetbeat_rare_user_agent.json | 0 .../modules/siem_winlogbeat/logo.json | 0 .../modules/siem_winlogbeat/manifest.json | 0 ...feed_rare_process_by_host_windows_ecs.json | 0 ...indows_anomalous_network_activity_ecs.json | 0 ...d_windows_anomalous_path_activity_ecs.json | 0 ...ndows_anomalous_process_all_hosts_ecs.json | 0 ...ed_windows_anomalous_process_creation.json | 0 .../ml/datafeed_windows_anomalous_script.json | 0 .../datafeed_windows_anomalous_service.json | 0 ...afeed_windows_anomalous_user_name_ecs.json | 0 ...atafeed_windows_rare_user_runas_event.json | 0 .../ml/rare_process_by_host_windows_ecs.json | 0 ...indows_anomalous_network_activity_ecs.json | 0 .../windows_anomalous_path_activity_ecs.json | 0 ...ndows_anomalous_process_all_hosts_ecs.json | 0 .../windows_anomalous_process_creation.json | 0 .../ml/windows_anomalous_script.json | 0 .../ml/windows_anomalous_service.json | 0 .../ml/windows_anomalous_user_name_ecs.json | 0 .../ml/windows_rare_user_runas_event.json | 0 .../modules/siem_winlogbeat_auth/logo.json | 0 .../siem_winlogbeat_auth/manifest.json | 0 ...windows_rare_user_type10_remote_login.json | 0 ...windows_rare_user_type10_remote_login.json | 0 .../models/data_visualizer/data_visualizer.ts | 4 +- .../ml/server/models/data_visualizer/index.ts | 0 .../models/fields_service/fields_service.d.ts | 0 .../models/fields_service/fields_service.js | 0 .../ml/server/models/fields_service/index.ts | 0 .../file_data_visualizer.ts | 2 +- .../file_data_visualizer/import_data.ts | 2 +- .../models/file_data_visualizer/index.ts | 0 .../ml/server/models/filter/filter_manager.ts | 5 +- .../plugins/ml/server/models/filter/index.js | 0 .../plugins/ml/server/models/filter/index.ts | 0 .../server/models/job_audit_messages/index.ts | 0 .../job_audit_messages.d.ts | 0 .../job_audit_messages/job_audit_messages.js | 2 +- .../ml/server/models/job_service/datafeeds.js | 5 +- .../server/models/job_service/error_utils.js | 5 +- .../ml/server/models/job_service/groups.js | 2 +- .../ml/server/models/job_service/index.js | 0 .../ml/server/models/job_service/jobs.js | 7 +- .../new_job/categorization/examples.ts | 6 +- .../new_job/categorization/index.ts | 0 .../new_job/categorization/top_categories.ts | 9 +- .../categorization/validation_results.ts | 6 +- .../models/job_service/new_job/charts.ts | 2 +- .../models/job_service/new_job/index.ts | 0 .../models/job_service/new_job/line_chart.ts | 9 +- .../job_service/new_job/population_chart.ts | 9 +- .../responses/cloudwatch_field_caps.json | 0 .../responses/farequote_field_caps.json | 0 .../responses/kibana_saved_objects.json | 0 .../__mocks__/responses/rollup_caps.json | 0 .../results/cloudwatch_rollup_job_caps.json | 0 .../__mocks__/results/farequote_job_caps.json | 0 .../results/farequote_job_caps_empty.json | 0 .../job_service/new_job_caps/aggregations.ts | 7 +- .../job_service/new_job_caps/field_service.ts | 6 +- .../models/job_service/new_job_caps/index.ts | 0 .../new_job_caps/new_job_caps.test.ts | 0 .../job_service/new_job_caps/new_job_caps.ts | 6 +- .../models/job_service/new_job_caps/rollup.ts | 4 +- .../__tests__/job_validation.js | 0 .../__tests__/mock_farequote_cardinality.json | 0 .../mock_farequote_search_response.json | 0 .../__tests__/mock_field_caps.json | 0 .../__tests__/mock_it_search_response.json | 0 .../__tests__/mock_time_field.json | 0 .../__tests__/mock_time_field_nested.json | 0 .../__tests__/mock_time_range.json | 0 .../__tests__/validate_bucket_span.js | 2 +- .../__tests__/validate_cardinality.js | 0 .../__tests__/validate_influencers.js | 0 .../__tests__/validate_model_memory_limit.js | 0 .../__tests__/validate_time_range.js | 0 .../ml/server/models/job_validation/index.ts | 0 .../models/job_validation/job_validation.d.ts | 4 +- .../models/job_validation/job_validation.js | 13 +- .../server/models/job_validation/messages.js | 2 +- .../job_validation/validate_bucket_span.js | 10 +- .../job_validation/validate_cardinality.d.ts | 5 +- .../job_validation/validate_cardinality.js | 0 .../job_validation/validate_influencers.js | 0 .../job_validation/validate_job_object.js | 0 .../validate_model_memory_limit.js | 2 +- .../job_validation/validate_time_range.js | 4 +- .../build_anomaly_table_items.d.ts | 2 +- .../build_anomaly_table_items.js | 2 +- .../get_partition_fields_values.ts | 4 +- .../ml/server/models/results_service/index.ts | 0 .../models/results_service/results_service.ts | 6 +- x-pack/plugins/ml/server/plugin.ts | 168 +++++++++++++ .../plugins/ml/server/routes/README.md | 0 .../plugins/ml/server/routes/annotations.ts | 21 +- .../ml/server/routes/anomaly_detectors.ts | 38 +-- .../plugins/ml/server/routes/apidoc.json | 0 .../plugins/ml/server/routes/calendars.ts | 18 +- .../ml/server/routes/data_frame_analytics.ts | 30 +-- .../ml/server/routes/data_visualizer.ts | 12 +- .../plugins/ml/server/routes/datafeeds.ts | 28 +-- .../ml/server/routes/fields_service.ts | 12 +- .../ml/server/routes/file_data_visualizer.ts | 20 +- .../plugins/ml/server/routes/filters.ts | 20 +- .../plugins/ml/server/routes/indices.ts | 8 +- .../ml/server/routes/job_audit_messages.ts | 10 +- .../plugins/ml/server/routes/job_service.ts | 48 ++-- .../ml/server/routes/job_validation.ts | 26 +- .../license_check_pre_routing_factory.ts} | 12 +- .../plugins/ml/server/routes/modules.ts | 18 +- .../ml/server/routes/notification_settings.ts | 8 +- .../ml/server/routes/results_service.ts | 18 +- .../routes/schemas}/annotations_schema.ts | 0 .../schemas}/anomaly_detectors_schema.ts | 0 .../routes/schemas}/calendars_schema.ts | 0 .../routes/schemas}/data_analytics_schema.ts | 0 .../routes/schemas}/data_visualizer_schema.ts | 0 .../routes/schemas}/datafeeds_schema.ts | 0 .../routes/schemas}/fields_service_schema.ts | 0 .../server/routes/schemas}/filters_schema.ts | 0 .../routes/schemas}/job_service_schema.ts | 0 .../routes/schemas}/job_validation_schema.ts | 0 .../ml/server/routes/schemas}/modules.ts | 0 .../routes/schemas}/results_service_schema.ts | 0 .../plugins/ml/server/routes/system.ts | 31 ++- x-pack/plugins/ml/server/types.ts | 43 ++++ 339 files changed, 655 insertions(+), 873 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/kibana.json delete mode 100644 x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts delete mode 100644 x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js delete mode 100644 x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts delete mode 100644 x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js delete mode 100644 x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js delete mode 100644 x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts delete mode 100644 x-pack/legacy/plugins/ml/server/lib/security_utils.js delete mode 100644 x-pack/legacy/plugins/ml/server/new_platform/plugin.ts create mode 100644 x-pack/plugins/ml/kibana.json rename x-pack/{legacy => }/plugins/ml/server/client/__tests__/elasticsearch_ml.js (100%) rename x-pack/{legacy => }/plugins/ml/server/client/elasticsearch_ml.js (100%) rename x-pack/{legacy => }/plugins/ml/server/client/error_wrapper.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/client/errors.js (100%) rename x-pack/{legacy => }/plugins/ml/server/client/log.ts (94%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server}/index.ts (57%) rename x-pack/{legacy => }/plugins/ml/server/lib/__tests__/query_utils.js (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_annotations/index.d.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_annotations/index.js (95%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_license/check_license.test.ts (81%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_license/check_license.ts (75%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_license/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_privileges/check_privileges.test.ts (91%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_privileges/check_privileges.ts (94%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_privileges/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_privileges/privileges.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/check_privileges/upgrade.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/ml_telemetry/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts (85%) rename x-pack/{legacy => }/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts (74%) rename x-pack/{legacy => }/plugins/ml/server/lib/query_utils.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/sample_data_sets/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/lib/spaces_utils.ts (75%) rename x-pack/{legacy => }/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/annotation_service/annotation.test.ts (96%) rename x-pack/{legacy => }/plugins/ml/server/models/annotation_service/annotation.ts (96%) rename x-pack/{legacy => }/plugins/ml/server/models/annotation_service/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts (75%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js (98%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/intervals.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/calculate_model_memory_limit/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/calendar/calendar_manager.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/calendar/event_manager.ts (94%) rename x-pack/{legacy => }/plugins/ml/server/models/calendar/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts (88%) rename x-pack/{legacy => }/plugins/ml/server/models/data_frame_analytics/index.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts (97%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/data_recognizer.ts (99%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/data_visualizer/data_visualizer.ts (99%) rename x-pack/{legacy => }/plugins/ml/server/models/data_visualizer/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/fields_service/fields_service.d.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/fields_service/fields_service.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/fields_service/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts (95%) rename x-pack/{legacy => }/plugins/ml/server/models/file_data_visualizer/import_data.ts (97%) rename x-pack/{legacy => }/plugins/ml/server/models/file_data_visualizer/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/filter/filter_manager.ts (98%) rename x-pack/{legacy => }/plugins/ml/server/models/filter/index.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/filter/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_audit_messages/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_audit_messages/job_audit_messages.js (98%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/datafeeds.js (97%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/error_utils.js (94%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/groups.js (95%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/index.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/jobs.js (98%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/categorization/examples.ts (95%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/categorization/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts (92%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts (96%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/charts.ts (87%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/line_chart.ts (92%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job/population_chart.ts (95%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts (97%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/field_service.ts (96%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts (93%) rename x-pack/{legacy => }/plugins/ml/server/models/job_service/new_job_caps/rollup.ts (92%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/job_validation.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js (98%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/job_validation.d.ts (83%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/job_validation.js (94%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/messages.js (99%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_bucket_span.js (93%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_cardinality.d.ts (78%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_cardinality.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_influencers.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_job_object.js (100%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_model_memory_limit.js (98%) rename x-pack/{legacy => }/plugins/ml/server/models/job_validation/validate_time_range.js (93%) rename x-pack/{legacy => }/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts (89%) rename x-pack/{legacy => }/plugins/ml/server/models/results_service/build_anomaly_table_items.js (99%) rename x-pack/{legacy => }/plugins/ml/server/models/results_service/get_partition_fields_values.ts (95%) rename x-pack/{legacy => }/plugins/ml/server/models/results_service/index.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/models/results_service/results_service.ts (97%) create mode 100644 x-pack/plugins/ml/server/plugin.ts rename x-pack/{legacy => }/plugins/ml/server/routes/README.md (100%) rename x-pack/{legacy => }/plugins/ml/server/routes/annotations.ts (83%) rename x-pack/{legacy => }/plugins/ml/server/routes/anomaly_detectors.ts (89%) rename x-pack/{legacy => }/plugins/ml/server/routes/apidoc.json (100%) rename x-pack/{legacy => }/plugins/ml/server/routes/calendars.ts (84%) rename x-pack/{legacy => }/plugins/ml/server/routes/data_frame_analytics.ts (89%) rename x-pack/{legacy => }/plugins/ml/server/routes/data_visualizer.ts (89%) rename x-pack/{legacy => }/plugins/ml/server/routes/datafeeds.ts (86%) rename x-pack/{legacy => }/plugins/ml/server/routes/fields_service.ts (84%) rename x-pack/{legacy => }/plugins/ml/server/routes/file_data_visualizer.ts (87%) rename x-pack/{legacy => }/plugins/ml/server/routes/filters.ts (87%) rename x-pack/{legacy => }/plugins/ml/server/routes/indices.ts (80%) rename x-pack/{legacy => }/plugins/ml/server/routes/job_audit_messages.ts (84%) rename x-pack/{legacy => }/plugins/ml/server/routes/job_service.ts (88%) rename x-pack/{legacy => }/plugins/ml/server/routes/job_validation.ts (85%) rename x-pack/{legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts => plugins/ml/server/routes/license_check_pre_routing_factory.ts} (71%) rename x-pack/{legacy => }/plugins/ml/server/routes/modules.ts (88%) rename x-pack/{legacy => }/plugins/ml/server/routes/notification_settings.ts (75%) rename x-pack/{legacy => }/plugins/ml/server/routes/results_service.ts (88%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/annotations_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/anomaly_detectors_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/calendars_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/data_analytics_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/data_visualizer_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/datafeeds_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/fields_service_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/filters_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/job_service_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/job_validation_schema.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/modules.ts (100%) rename x-pack/{legacy/plugins/ml/server/new_platform => plugins/ml/server/routes/schemas}/results_service_schema.ts (100%) rename x-pack/{legacy => }/plugins/ml/server/routes/system.ts (88%) create mode 100644 x-pack/plugins/ml/server/types.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index bb084b3bb72a18..66342266f1dbc6 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -26,7 +26,7 @@ "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": "legacy/plugins/maps", - "xpack.ml": "legacy/plugins/ml", + "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], diff --git a/x-pack/legacy/plugins/ml/common/constants/app.ts b/x-pack/legacy/plugins/ml/common/constants/app.ts index 140a709b0c42be..bbec35a17faa54 100644 --- a/x-pack/legacy/plugins/ml/common/constants/app.ts +++ b/x-pack/legacy/plugins/ml/common/constants/app.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const API_BASE_PATH = '/api/transform/'; +export const PLUGIN_ID = 'ml'; diff --git a/x-pack/legacy/plugins/ml/common/constants/license.ts b/x-pack/legacy/plugins/ml/common/constants/license.ts index 2027e2c8b18653..183844c9ef980c 100644 --- a/x-pack/legacy/plugins/ml/common/constants/license.ts +++ b/x-pack/legacy/plugins/ml/common/constants/license.ts @@ -8,3 +8,5 @@ export enum LICENSE_TYPE { BASIC, FULL, // >= platinum } + +export const VALID_FULL_LICENSE_MODES = ['platinum', 'enterprise', 'trial']; diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 09f1b9ccedce4f..47df7c8c3e5e62 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -6,23 +6,13 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; -import KbnServer, { Server } from 'src/legacy/server/kbn_server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { plugin } from './server/new_platform'; -import { CloudSetup } from '../../../plugins/cloud/server'; +import { Server } from 'src/legacy/server/kbn_server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; -import { - MlInitializerContext, - MlCoreSetup, - MlHttpServiceSetup, -} from './server/new_platform/plugin'; +// @ts-ignore: could not find declaration file for module +import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; // @ts-ignore: could not find declaration file for module import mappings from './mappings'; -interface MlServer extends Server { - addAppLinksToSampleDataset: () => {}; -} - export const ml = (kibana: any) => { return new kibana.Plugin({ require: ['kibana', 'elasticsearch', 'xpack_main'], @@ -60,43 +50,8 @@ export const ml = (kibana: any) => { }, }, - async init(server: MlServer) { - const kbnServer = (server as unknown) as KbnServer; - - const initializerContext = ({ - legacyConfig: server.config(), - logger: { - get(...contextParts: string[]) { - return kbnServer.newPlatform.coreContext.logger.get('plugins', 'ml', ...contextParts); - }, - }, - } as unknown) as MlInitializerContext; - - const mlHttpService: MlHttpServiceSetup = { - ...kbnServer.newPlatform.setup.core.http, - route: server.route.bind(server), - }; - - const core: MlCoreSetup = { - injectUiAppVars: server.injectUiAppVars, - http: mlHttpService, - savedObjects: server.savedObjects, - coreSavedObjects: kbnServer.newPlatform.start.core.savedObjects, - elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, - }; - const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins; - const plugins = { - elasticsearch: server.plugins.elasticsearch, // legacy - security: server.newPlatform.setup.plugins.security, - xpackMain: server.plugins.xpack_main, - spaces: server.plugins.spaces, - home, - usageCollection: usageCollection as UsageCollectionSetup, - cloud: cloud as CloudSetup, - ml: this, - }; - - plugin(initializerContext).setup(core, plugins); + async init(server: Server) { + mirrorPluginStatus(server.plugins.xpack_main, this); }, }); }; diff --git a/x-pack/legacy/plugins/ml/kibana.json b/x-pack/legacy/plugins/ml/kibana.json deleted file mode 100644 index f36b4848186906..00000000000000 --- a/x-pack/legacy/plugins/ml/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "ml", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["ml"], - "server": true, - "ui": true -} diff --git a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index 96e6aab3779621..4af753ddb4d1f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -82,9 +82,16 @@ function setLicenseExpired(features: any) { } } } - +// Temporary hack for cutting over server to NP function getFeatures() { - return xpackInfo.get('features.ml'); + return { + isAvailable: true, + showLinks: true, + enableLinks: true, + licenseType: 1, + hasExpired: false, + }; + // return xpackInfo.get('features.ml'); } function redirectToKibana() { diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts deleted file mode 100644 index bf2e656afff123..00000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts +++ /dev/null @@ -1,9 +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 { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; - -export function callWithInternalUserFactory(elasticsearchPlugin: ElasticsearchPlugin): any; diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js deleted file mode 100644 index 2e5431bdd6ce2d..00000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; - -const _callWithInternalUser = once(elasticsearchPlugin => { - const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = elasticsearchPlugin => { - return (...args) => { - return _callWithInternalUser(elasticsearchPlugin)(...args); - }; -}; diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts deleted file mode 100644 index be016cc13ed0f3..00000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithInternalUserFactory } from './call_with_internal_user_factory'; - -describe('call_with_internal_user_factory', () => { - describe('callWithInternalUserFactory', () => { - let elasticsearchPlugin: any; - let callWithInternalUser: any; - - beforeEach(() => { - callWithInternalUser = jest.fn(); - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - }); - - it('should use internal user "admin"', () => { - const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); - callWithInternalUserInstance(); - - expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js deleted file mode 100644 index b39a58b317500f..00000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from './elasticsearch_ml'; - -const callWithRequest = once(elasticsearchPlugin => { - const config = { plugins: [elasticsearchJsPlugin] }; - const cluster = elasticsearchPlugin.createCluster('ml', config); - - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (elasticsearchPlugin, request) => { - return (...args) => { - return callWithRequest(elasticsearchPlugin)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js b/x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js deleted file mode 100644 index 6e0181f49072e1..00000000000000 --- a/x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { isSecurityDisabled } from '../security_utils'; - -describe('ML - security utils', () => { - function mockXpackMainPluginFactory(isAvailable = true, isEnabled = true) { - return { - info: { - isAvailable: () => isAvailable, - feature: () => ({ - isEnabled: () => isEnabled, - }), - }, - }; - } - - describe('isSecurityDisabled', () => { - it('returns not disabled for given mock server object #1', () => { - expect(isSecurityDisabled(mockXpackMainPluginFactory())).to.be(false); - }); - - it('returns not disabled for given mock server object #2', () => { - expect(isSecurityDisabled(mockXpackMainPluginFactory(false))).to.be(false); - }); - - it('returns disabled for given mock server object #3', () => { - expect(isSecurityDisabled(mockXpackMainPluginFactory(true, false))).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts b/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts deleted file mode 100644 index 26fdff73b34606..00000000000000 --- a/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts +++ /dev/null @@ -1,9 +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 { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; - -export function isSecurityDisabled(xpackMainPlugin: XPackMainPlugin): boolean; diff --git a/x-pack/legacy/plugins/ml/server/lib/security_utils.js b/x-pack/legacy/plugins/ml/server/lib/security_utils.js deleted file mode 100644 index 27109e645b185b..00000000000000 --- a/x-pack/legacy/plugins/ml/server/lib/security_utils.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * Contains utility functions related to x-pack security. - */ - -export function isSecurityDisabled(xpackMainPlugin) { - const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; - // we assume that `xpack.isAvailable()` always returns `true` because we're inside x-pack - // if for whatever reason it returns `false`, `isSecurityDisabled()` would also return `false` - // which would result in follow-up behavior assuming security is enabled. This is intentional, - // because it results in more defensive behavior. - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - return securityInfo && securityInfo.isEnabled() === false; -} diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts deleted file mode 100644 index 43c276ac63a13d..00000000000000 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ /dev/null @@ -1,238 +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 { i18n } from '@kbn/i18n'; -import { ServerRoute } from 'hapi'; -import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; -import { - Logger, - PluginInitializerContext, - CoreSetup, - IRouter, - IScopedClusterClient, - SavedObjectsServiceStart, -} from 'src/core/server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ElasticsearchServiceSetup } from 'src/core/server'; -import { CloudSetup } from '../../../../../plugins/cloud/server'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; -import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; -import { checkLicense } from '../lib/check_license'; -// @ts-ignore: could not find declaration file for module -import { mirrorPluginStatus } from '../../../../server/lib/mirror_plugin_status'; -import { LICENSE_TYPE } from '../../common/constants/license'; -import { annotationRoutes } from '../routes/annotations'; -import { jobRoutes } from '../routes/anomaly_detectors'; -import { dataFeedRoutes } from '../routes/datafeeds'; -import { indicesRoutes } from '../routes/indices'; -import { jobValidationRoutes } from '../routes/job_validation'; -import { makeMlUsageCollector } from '../lib/ml_telemetry'; -import { notificationRoutes } from '../routes/notification_settings'; -import { systemRoutes } from '../routes/system'; -import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics'; -import { dataRecognizer } from '../routes/modules'; -import { dataVisualizerRoutes } from '../routes/data_visualizer'; -import { calendars } from '../routes/calendars'; -// @ts-ignore: could not find declaration file for module -import { fieldsService } from '../routes/fields_service'; -import { filtersRoutes } from '../routes/filters'; -import { resultsServiceRoutes } from '../routes/results_service'; -import { jobServiceRoutes } from '../routes/job_service'; -import { jobAuditMessagesRoutes } from '../routes/job_audit_messages'; -import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer'; -import { initMlServerLog, LogInitialization } from '../client/log'; -import { HomeServerPluginSetup } from '../../../../../../src/plugins/home/server'; -// @ts-ignore: could not find declaration file for module -import { elasticsearchJsPlugin } from '../client/elasticsearch_ml'; - -export const PLUGIN_ID = 'ml'; - -type CoreHttpSetup = CoreSetup['http']; -export interface MlHttpServiceSetup extends CoreHttpSetup { - route(route: ServerRoute | ServerRoute[]): void; -} - -export interface MlXpackMainPlugin extends XPackMainPlugin { - status?: any; -} - -export interface MlCoreSetup { - injectUiAppVars: (id: string, callback: () => {}) => any; - http: MlHttpServiceSetup; - savedObjects: SavedObjectsLegacyService; - coreSavedObjects: SavedObjectsServiceStart; - elasticsearch: ElasticsearchServiceSetup; -} -export interface MlInitializerContext extends PluginInitializerContext { - legacyConfig: KibanaConfig; - log: Logger; -} -export interface PluginsSetup { - elasticsearch: ElasticsearchPlugin; - xpackMain: MlXpackMainPlugin; - security: any; - spaces: any; - usageCollection?: UsageCollectionSetup; - cloud?: CloudSetup; - home?: HomeServerPluginSetup; - // TODO: this is temporary for `mirrorPluginStatus` - ml: any; -} - -export interface RouteInitialization { - commonRouteConfig: any; - config?: any; - elasticsearchPlugin: ElasticsearchPlugin; - elasticsearchService: ElasticsearchServiceSetup; - route(route: ServerRoute | ServerRoute[]): void; - router: IRouter; - xpackMainPlugin: MlXpackMainPlugin; - savedObjects?: SavedObjectsServiceStart; - spacesPlugin: any; - securityPlugin: any; - cloud?: CloudSetup; -} - -declare module 'kibana/server' { - interface RequestHandlerContext { - ml?: { - mlClient: IScopedClusterClient; - }; - } -} - -export class Plugin { - private readonly pluginId: string = PLUGIN_ID; - private config: any; - private log: Logger; - - constructor(initializerContext: MlInitializerContext) { - this.config = initializerContext.legacyConfig; - this.log = initializerContext.logger.get(); - } - - public setup(core: MlCoreSetup, plugins: PluginsSetup) { - const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; - const { http, coreSavedObjects } = core; - const pluginId = this.pluginId; - - mirrorPluginStatus(xpackMainPlugin, plugins.ml); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - const mlFeature = xpackMainPlugin.info.feature(pluginId); - mlFeature.registerLicenseCheckResultsGenerator(checkLicense); - - // Add links to the Kibana sample data sets if ml is enabled - // and there is a full license (trial or platinum). - if (mlFeature.isEnabled() === true && plugins.home) { - const licenseCheckResults = mlFeature.getLicenseCheckResults(); - if (licenseCheckResults.licenseType === LICENSE_TYPE.FULL) { - addLinksToSampleDatasets({ - addAppLinksToSampleDataset: plugins.home.sampleData.addAppLinksToSampleDataset, - }); - } - } - }); - - xpackMainPlugin.registerFeature({ - id: 'ml', - name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { - defaultMessage: 'Machine Learning', - }), - icon: 'machineLearningApp', - navLinkId: 'ml', - app: ['ml', 'kibana'], - catalogue: ['ml'], - privileges: {}, - reserved: { - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - description: i18n.translate('xpack.ml.feature.reserved.description', { - defaultMessage: - 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.', - }), - }, - }); - - // Add server routes and initialize the plugin here - const commonRouteConfig = { - pre: [ - function forbidApiAccess() { - const licenseCheckResults = xpackMainPlugin.info - .feature(pluginId) - .getLicenseCheckResults(); - if (licenseCheckResults.isAvailable) { - return null; - } else { - throw Boom.forbidden(licenseCheckResults.message); - } - }, - ], - }; - - // Can access via new platform router's handler function 'context' parameter - context.ml.mlClient - const mlClient = core.elasticsearch.createClient('ml', { plugins: [elasticsearchJsPlugin] }); - http.registerRouteHandlerContext('ml', (context, request) => { - return { - mlClient: mlClient.asScoped(request), - }; - }); - - const routeInitializationDeps: RouteInitialization = { - commonRouteConfig, - route: http.route, - router: http.createRouter(), - elasticsearchPlugin: plugins.elasticsearch, - elasticsearchService: core.elasticsearch, - xpackMainPlugin: plugins.xpackMain, - spacesPlugin: plugins.spaces, - securityPlugin: plugins.security, - }; - - const extendedRouteInitializationDeps: RouteInitialization = { - ...routeInitializationDeps, - config: this.config, - savedObjects: coreSavedObjects, - spacesPlugin: plugins.spaces, - cloud: plugins.cloud, - }; - - const logInitializationDeps: LogInitialization = { - log: this.log, - }; - - annotationRoutes(routeInitializationDeps); - jobRoutes(routeInitializationDeps); - dataFeedRoutes(routeInitializationDeps); - dataFrameAnalyticsRoutes(routeInitializationDeps); - indicesRoutes(routeInitializationDeps); - jobValidationRoutes(extendedRouteInitializationDeps); - notificationRoutes(routeInitializationDeps); - systemRoutes(extendedRouteInitializationDeps); - dataRecognizer(extendedRouteInitializationDeps); - dataVisualizerRoutes(routeInitializationDeps); - calendars(routeInitializationDeps); - fieldsService(routeInitializationDeps); - filtersRoutes(routeInitializationDeps); - resultsServiceRoutes(routeInitializationDeps); - jobServiceRoutes(routeInitializationDeps); - jobAuditMessagesRoutes(routeInitializationDeps); - fileDataVisualizerRoutes(extendedRouteInitializationDeps); - - initMlServerLog(logInitializationDeps); - makeMlUsageCollector(plugins.usageCollection, coreSavedObjects); - } - - public stop() {} -} diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json new file mode 100644 index 00000000000000..e944af6821c0b6 --- /dev/null +++ b/x-pack/plugins/ml/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "ml", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ml"], + "requiredPlugins": ["cloud", "features", "home", "licensing", "security", "spaces", "usageCollection"], + "server": true, + "ui": false +} diff --git a/x-pack/legacy/plugins/ml/server/client/__tests__/elasticsearch_ml.js b/x-pack/plugins/ml/server/client/__tests__/elasticsearch_ml.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/__tests__/elasticsearch_ml.js rename to x-pack/plugins/ml/server/client/__tests__/elasticsearch_ml.js diff --git a/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js b/x-pack/plugins/ml/server/client/elasticsearch_ml.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js rename to x-pack/plugins/ml/server/client/elasticsearch_ml.js diff --git a/x-pack/legacy/plugins/ml/server/client/error_wrapper.ts b/x-pack/plugins/ml/server/client/error_wrapper.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/error_wrapper.ts rename to x-pack/plugins/ml/server/client/error_wrapper.ts diff --git a/x-pack/legacy/plugins/ml/server/client/errors.js b/x-pack/plugins/ml/server/client/errors.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/errors.js rename to x-pack/plugins/ml/server/client/errors.js diff --git a/x-pack/legacy/plugins/ml/server/client/log.ts b/x-pack/plugins/ml/server/client/log.ts similarity index 94% rename from x-pack/legacy/plugins/ml/server/client/log.ts rename to x-pack/plugins/ml/server/client/log.ts index ae82383ead6050..8ee5882f6c2c16 100644 --- a/x-pack/legacy/plugins/ml/server/client/log.ts +++ b/x-pack/plugins/ml/server/client/log.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../../src/core/server'; +import { Logger } from '../../../../../src/core/server'; export interface LogInitialization { log: Logger; diff --git a/x-pack/legacy/plugins/ml/server/new_platform/index.ts b/x-pack/plugins/ml/server/index.ts similarity index 57% rename from x-pack/legacy/plugins/ml/server/new_platform/index.ts rename to x-pack/plugins/ml/server/index.ts index b03f2dac613b03..55e87ed6f0c6a3 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, MlInitializerContext } from './plugin'; +import { PluginInitializerContext } from 'kibana/server'; +import { MlServerPlugin } from './plugin'; -export function plugin(initializerContext: MlInitializerContext) { - return new Plugin(initializerContext); -} +export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/ml/server/lib/__tests__/query_utils.js b/x-pack/plugins/ml/server/lib/__tests__/query_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/__tests__/query_utils.js rename to x-pack/plugins/ml/server/lib/__tests__/query_utils.js diff --git a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts b/x-pack/plugins/ml/server/lib/check_annotations/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts rename to x-pack/plugins/ml/server/lib/check_annotations/index.d.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js b/x-pack/plugins/ml/server/lib/check_annotations/index.js similarity index 95% rename from x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js rename to x-pack/plugins/ml/server/lib/check_annotations/index.js index 186c27b0326d70..55a90c0cec322c 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.js @@ -10,7 +10,7 @@ import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, ML_ANNOTATIONS_INDEX_PATTERN, -} from '../../../common/constants/index_patterns'; +} from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; // Annotations Feature is available if: // - ML_ANNOTATIONS_INDEX_PATTERN index is present diff --git a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.test.ts b/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts similarity index 81% rename from x-pack/legacy/plugins/ml/server/lib/check_license/check_license.test.ts rename to x-pack/plugins/ml/server/lib/check_license/check_license.test.ts index 1d80a226486bb1..942dbe37226175 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.test.ts +++ b/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts @@ -7,12 +7,12 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { set } from 'lodash'; -import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info'; +import { LicenseCheckResult } from '../../types'; import { checkLicense } from './check_license'; describe('check_license', () => { - let mockLicenseInfo: XPackInfo; - beforeEach(() => (mockLicenseInfo = {} as XPackInfo)); + let mockLicenseInfo: LicenseCheckResult; + beforeEach(() => (mockLicenseInfo = {} as LicenseCheckResult)); describe('license information is undefined', () => { it('should set isAvailable to false', () => { @@ -33,7 +33,9 @@ describe('check_license', () => { }); describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); + beforeEach(() => { + mockLicenseInfo.isAvailable = false; + }); it('should set isAvailable to false', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); @@ -54,8 +56,8 @@ describe('check_license', () => { describe('license information is available', () => { beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); + mockLicenseInfo.isAvailable = true; + mockLicenseInfo.type = 'basic'; }); describe('& ML is disabled in Elasticsearch', () => { @@ -66,7 +68,7 @@ describe('check_license', () => { sinon .stub() .withArgs('ml') - .returns({ isEnabled: () => false }) + .returns({ isEnabled: false }) ); }); @@ -89,21 +91,17 @@ describe('check_license', () => { describe('& ML is enabled in Elasticsearch', () => { beforeEach(() => { - set( - mockLicenseInfo, - 'feature', - sinon - .stub() - .withArgs('ml') - .returns({ isEnabled: () => true }) - ); + mockLicenseInfo.isEnabled = true; }); describe('& license is >= platinum', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - + beforeEach(() => { + mockLicenseInfo.type = 'platinum'; + }); describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); + beforeEach(() => { + mockLicenseInfo.isActive = true; + }); it('should set isAvailable to true', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); @@ -123,7 +121,9 @@ describe('check_license', () => { }); describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); + beforeEach(() => { + mockLicenseInfo.isActive = false; + }); it('should set isAvailable to true', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); @@ -144,10 +144,14 @@ describe('check_license', () => { }); describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => false)); + beforeEach(() => { + mockLicenseInfo.type = 'basic'; + }); describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); + beforeEach(() => { + mockLicenseInfo.isActive = true; + }); it('should set isAvailable to true', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.ts b/x-pack/plugins/ml/server/lib/check_license/check_license.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/lib/check_license/check_license.ts rename to x-pack/plugins/ml/server/lib/check_license/check_license.ts index c88ab087a81985..5bf3d590a1912d 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.ts +++ b/x-pack/plugins/ml/server/lib/check_license/check_license.ts @@ -5,8 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { LICENSE_TYPE } from '../../../common/constants/license'; -import { XPackInfo } from '../../../../../../legacy/plugins/xpack_main/server/lib/xpack_info'; +import { + LICENSE_TYPE, + VALID_FULL_LICENSE_MODES, +} from '../../../../../legacy/plugins/ml/common/constants/license'; +import { LicenseCheckResult } from '../../types'; interface Response { isAvailable: boolean; @@ -17,10 +20,10 @@ interface Response { message?: string; } -export function checkLicense(xpackLicenseInfo: XPackInfo): Response { +export function checkLicense(licenseCheckResult: LicenseCheckResult): Response { // If, for some reason, we cannot get the license information // from Elasticsearch, assume worst case and disable the Machine Learning UI - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { + if (licenseCheckResult === undefined || !licenseCheckResult.isAvailable) { return { isAvailable: false, showLinks: true, @@ -35,7 +38,7 @@ export function checkLicense(xpackLicenseInfo: XPackInfo): Response { }; } - const featureEnabled = xpackLicenseInfo.feature('ml').isEnabled(); + const featureEnabled = licenseCheckResult.isEnabled; if (!featureEnabled) { return { isAvailable: false, @@ -47,12 +50,11 @@ export function checkLicense(xpackLicenseInfo: XPackInfo): Response { }; } - const VALID_FULL_LICENSE_MODES = ['platinum', 'enterprise', 'trial']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_FULL_LICENSE_MODES); + const isLicenseModeValid = + licenseCheckResult.type && VALID_FULL_LICENSE_MODES.includes(licenseCheckResult.type); const licenseType = isLicenseModeValid === true ? LICENSE_TYPE.FULL : LICENSE_TYPE.BASIC; - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseTypeName = xpackLicenseInfo.license.getType(); + const isLicenseActive = licenseCheckResult.isActive; + const licenseTypeName = licenseCheckResult.type; // Platinum or trial license is valid but not active, i.e. expired if (licenseType === LICENSE_TYPE.FULL && isLicenseActive === false) { diff --git a/x-pack/legacy/plugins/ml/server/lib/check_license/index.ts b/x-pack/plugins/ml/server/lib/check_license/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_license/index.ts rename to x-pack/plugins/ml/server/lib/check_license/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts b/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts rename to x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts similarity index 91% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.test.ts rename to x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts index da8ef25b2f4dff..0690aa53576a5a 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts @@ -8,81 +8,29 @@ import { callWithRequestProvider } from './__mocks__/call_with_request'; import { privilegesProvider } from './check_privileges'; import { mlPrivileges } from './privileges'; -const xpackMainPluginWithSecurity = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => true }; - } - }, - license: { - isOneOf: () => true, - isActive: () => true, - getType: () => 'platinum', - }, - }, -} as any; +const licenseCheckResultWithSecurity = { + isAvailable: true, + isEnabled: true, + isSecurityDisabled: false, + type: 'platinum', + isActive: true, +}; -const xpackMainPluginWithOutSecurity = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => false }; - } - }, - license: { - isOneOf: () => true, - isActive: () => true, - getType: () => 'platinum', - }, - }, -} as any; +const licenseCheckResultWithOutSecurity = { + ...licenseCheckResultWithSecurity, + isSecurityDisabled: true, +}; -const xpackMainPluginWithOutSecurityBasicLicense = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => false }; - } - }, - license: { - isOneOf: () => false, - isActive: () => true, - getType: () => 'basic', - }, - }, -} as any; +const licenseCheckResultWithOutSecurityBasicLicense = { + ...licenseCheckResultWithSecurity, + isSecurityDisabled: true, + type: 'basic', +}; -const xpackMainPluginWithSecurityBasicLicense = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => true }; - } - }, - license: { - isOneOf: () => false, - isActive: () => true, - getType: () => 'basic', - }, - }, -} as any; +const licenseCheckResultWithSecurityBasicLicense = { + ...licenseCheckResultWithSecurity, + type: 'basic', +}; const mlIsEnabled = async () => true; const mlIsNotEnabled = async () => false; @@ -99,7 +47,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities } = await getPrivileges(); @@ -114,7 +62,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -149,7 +97,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -184,7 +132,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -219,7 +167,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -254,7 +202,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurityBasicLicense, + licenseCheckResultWithSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -289,7 +237,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurityBasicLicense, + licenseCheckResultWithSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -324,7 +272,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsNotEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -361,7 +309,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -396,7 +344,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -431,7 +379,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -466,7 +414,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurityBasicLicense, + licenseCheckResultWithOutSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -501,7 +449,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurityBasicLicense, + licenseCheckResultWithOutSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -536,7 +484,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsNotEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts similarity index 94% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts rename to x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts index 617778afbe121b..a427780d13344a 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts @@ -5,12 +5,14 @@ */ import { IScopedClusterClient } from 'kibana/server'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { XPackMainPlugin } from '../../../../xpack_main/server/xpack_main'; -import { isSecurityDisabled } from '../../lib/security_utils'; +import { + Privileges, + getDefaultPrivileges, +} from '../../../../../legacy/plugins/ml/common/types/privileges'; import { upgradeCheckProvider } from './upgrade'; import { checkLicense } from '../check_license'; -import { LICENSE_TYPE } from '../../../common/constants/license'; +import { LICENSE_TYPE } from '../../../../../legacy/plugins/ml/common/constants/license'; +import { LicenseCheckResult } from '../../types'; import { mlPrivileges } from './privileges'; @@ -25,7 +27,7 @@ interface Response { export function privilegesProvider( callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - xpackMainPlugin: XPackMainPlugin, + licenseCheckResult: LicenseCheckResult, isMlEnabledInSpace: () => Promise, ignoreSpaces: boolean = false ) { @@ -35,8 +37,8 @@ export function privilegesProvider( const privileges = getDefaultPrivileges(); const upgradeInProgress = await isUpgradeInProgress(); - const securityDisabled = isSecurityDisabled(xpackMainPlugin); - const license = checkLicense(xpackMainPlugin.info); + const securityDisabled = licenseCheckResult.isSecurityDisabled; + const license = checkLicense(licenseCheckResult); const isPlatinumOrTrialLicense = license.licenseType === LICENSE_TYPE.FULL; const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/index.ts b/x-pack/plugins/ml/server/lib/check_privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/index.ts rename to x-pack/plugins/ml/server/lib/check_privileges/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/privileges.ts rename to x-pack/plugins/ml/server/lib/check_privileges/privileges.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/upgrade.ts b/x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/upgrade.ts rename to x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts similarity index 85% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts index 9d14ffb31be631..c03396445f8687 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts @@ -55,10 +55,9 @@ describe('ml_telemetry', () => { }); describe('incrementFileDataVisualizerIndexCreationCount', () => { - let savedObjects: any; - let internalRepository: any; + let savedObjectsClient: any; - function createInternalRepositoryInstance( + function createSavedObjectsClientInstance( telemetryEnabled?: boolean, indexCreationCount?: number ) { @@ -93,42 +92,39 @@ describe('ml_telemetry', () => { } function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void { - internalRepository = createInternalRepositoryInstance(telemetryEnabled, indexCreationCount); - savedObjects = { - createInternalRepository: jest.fn(() => internalRepository), - }; + savedObjectsClient = createSavedObjectsClientInstance(telemetryEnabled, indexCreationCount); } it('should not increment if telemetry status cannot be determined', async () => { mockInit(); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls).toHaveLength(0); + expect(savedObjectsClient.create.mock.calls).toHaveLength(0); }); it('should not increment if telemetry status is disabled', async () => { mockInit(false); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls).toHaveLength(0); + expect(savedObjectsClient.create.mock.calls).toHaveLength(0); }); it('should initialize index_creation_count with 1', async () => { mockInit(true); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(internalRepository.create.mock.calls[0][1]).toEqual({ + expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 1 }, }); }); it('should increment index_creation_count to 2', async () => { mockInit(true, 1); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(internalRepository.create.mock.calls[0][1]).toEqual({ + expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 2 }, }); }); diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts similarity index 74% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts index d76b1ee94e21e9..8cf24213961b18 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SavedObjectAttributes, - SavedObjectsServiceStart, - ISavedObjectsRepository, -} from 'src/core/server'; +import { SavedObjectAttributes, SavedObjectsClientContract } from 'src/core/server'; export interface MlTelemetry extends SavedObjectAttributes { file_data_visualizer: { @@ -31,21 +27,20 @@ export function createMlTelemetry(count: number = 0): MlTelemetry { } // savedObjects export function storeMlTelemetry( - internalRepository: ISavedObjectsRepository, + savedObjectsClient: SavedObjectsClientContract, mlTelemetry: MlTelemetry ): void { - internalRepository.create('ml-telemetry', mlTelemetry, { + savedObjectsClient.create('ml-telemetry', mlTelemetry, { id: ML_TELEMETRY_DOC_ID, overwrite: true, }); } export async function incrementFileDataVisualizerIndexCreationCount( - savedObjects: SavedObjectsServiceStart + savedObjectsClient: SavedObjectsClientContract ): Promise { - const internalRepository = await savedObjects.createInternalRepository(); try { - const { attributes } = await internalRepository.get('telemetry', 'telemetry'); + const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry'); if (attributes.enabled === false) { return; @@ -59,7 +54,7 @@ export async function incrementFileDataVisualizerIndexCreationCount( let indicesCount = 1; try { - const { attributes } = (await internalRepository.get( + const { attributes } = (await savedObjectsClient.get( 'ml-telemetry', ML_TELEMETRY_DOC_ID )) as MlTelemetrySavedObject; @@ -69,5 +64,5 @@ export async function incrementFileDataVisualizerIndexCreationCount( } const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(internalRepository, mlTelemetry); + storeMlTelemetry(savedObjectsClient, mlTelemetry); } diff --git a/x-pack/legacy/plugins/ml/server/lib/query_utils.ts b/x-pack/plugins/ml/server/lib/query_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/query_utils.ts rename to x-pack/plugins/ml/server/lib/query_utils.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/sample_data_sets/index.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/sample_data_sets/index.ts rename to x-pack/plugins/ml/server/lib/sample_data_sets/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts rename to x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts b/x-pack/plugins/ml/server/lib/spaces_utils.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts rename to x-pack/plugins/ml/server/lib/spaces_utils.ts index 92373bae4ea1d0..ed684eadb95704 100644 --- a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts +++ b/x-pack/plugins/ml/server/lib/spaces_utils.ts @@ -5,20 +5,19 @@ */ import { Request } from 'hapi'; -import { Space } from '../../../../../plugins/spaces/server'; -import { LegacySpacesPlugin } from '../../../spaces'; +import { Space, SpacesPluginSetup } from '../../../spaces/server'; interface GetActiveSpaceResponse { valid: boolean; space?: Space; } -export function spacesUtilsProvider(spacesPlugin: LegacySpacesPlugin, request: Request) { +export function spacesUtilsProvider(spacesPlugin: SpacesPluginSetup, request: Request) { async function activeSpace(): Promise { try { return { valid: true, - space: await spacesPlugin.getActiveSpace(request), + space: await spacesPlugin.spacesService.getActiveSpace(request), }; } catch (e) { return { diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json b/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json rename to x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json b/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json rename to x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts rename to x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 7e0649d15bfb0e..d7a13154a6f378 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -8,9 +8,12 @@ import getAnnotationsRequestMock from './__mocks__/get_annotations_request.json' import getAnnotationsResponseMock from './__mocks__/get_annotations_response.json'; import { RequestHandlerContext } from 'src/core/server'; -import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; -import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE } from '../../../common/constants/index_patterns'; -import { Annotation, isAnnotations } from '../../../common/types/annotations'; +import { ANNOTATION_TYPE } from '../../../../../legacy/plugins/ml/common/constants/annotations'; +import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { + Annotation, + isAnnotations, +} from '../../../../../legacy/plugins/ml/common/types/annotations'; import { DeleteParams, GetResponse, IndexAnnotationArgs } from './annotation'; import { annotationServiceProvider } from './index'; diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts rename to x-pack/plugins/ml/server/models/annotation_service/annotation.ts index 399305ea2603eb..042d7bbc80653a 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -8,18 +8,18 @@ import Boom from 'boom'; import _ from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; -import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; +import { ANNOTATION_TYPE } from '../../../../../legacy/plugins/ml/common/constants/annotations'; import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, -} from '../../../common/constants/index_patterns'; +} from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; import { Annotation, Annotations, isAnnotation, isAnnotations, -} from '../../../common/types/annotations'; +} from '../../../../../legacy/plugins/ml/common/types/annotations'; // TODO All of the following interface/type definitions should // eventually be replaced by the proper upstream definitions diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts b/x-pack/plugins/ml/server/models/annotation_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts rename to x-pack/plugins/ml/server/models/annotation_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts index ea986feab4e997..e39a0177c31b95 100644 --- a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts @@ -5,10 +5,10 @@ */ import { APICaller } from 'src/core/server'; -import { BucketSpanEstimatorData } from '../../../public/application/services/ml_api_service'; +import { BucketSpanEstimatorData } from '../../../../../legacy/plugins/ml/public/application/services/ml_api_service'; export function estimateBucketSpanFactory( callAsCurrentUser: APICaller, callAsInternalUser: APICaller, - xpackMainPlugin: any + isSecurityDisabled: boolean ): (config: BucketSpanEstimatorData) => Promise; diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index aec677dd57d614..53b9d75304963b 100644 --- a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -12,9 +12,11 @@ import { INTERVALS } from './intervals'; import { singleSeriesCheckerFactory } from './single_series_checker'; import { polledDataCheckerFactory } from './polled_data_checker'; -import { isSecurityDisabled } from '../../lib/security_utils'; - -export function estimateBucketSpanFactory(callAsCurrentUser, callAsInternalUser, xpackMainPlugin) { +export function estimateBucketSpanFactory( + callAsCurrentUser, + callAsInternalUser, + isSecurityDisabled +) { const PolledDataChecker = polledDataCheckerFactory(callAsCurrentUser); const SingleSeriesChecker = singleSeriesCheckerFactory(callAsCurrentUser); @@ -384,7 +386,7 @@ export function estimateBucketSpanFactory(callAsCurrentUser, callAsInternalUser, }); } - if (isSecurityDisabled(xpackMainPlugin)) { + if (isSecurityDisabled) { getBucketSpanEstimation(); } else { // if security is enabled, check that the user has permission to diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/index.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/index.ts rename to x-pack/plugins/ml/server/models/bucket_span_estimator/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/intervals.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/intervals.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/intervals.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/intervals.js diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js diff --git a/x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts rename to x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts diff --git a/x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js rename to x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js diff --git a/x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/index.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/index.ts rename to x-pack/plugins/ml/server/models/calculate_model_memory_limit/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts rename to x-pack/plugins/ml/server/models/calendar/calendar_manager.ts diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts similarity index 94% rename from x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts rename to x-pack/plugins/ml/server/models/calendar/event_manager.ts index 488839f68b3fe2..0a3108016da0e3 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; -import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; +import { GLOBAL_CALENDAR } from '../../../../../legacy/plugins/ml/common/constants/calendars'; export interface CalendarEvent { calendar_id?: string; diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/index.ts b/x-pack/plugins/ml/server/models/calendar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calendar/index.ts rename to x-pack/plugins/ml/server/models/calendar/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts rename to x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index abe389165182f0..a8757e289dcf75 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { callWithRequestType } from '../../../common/types/kibana'; -import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { JobMessage } from '../../../common/types/audit_message'; +import { callWithRequestType } from '../../../../../legacy/plugins/ml/common/types/kibana'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { JobMessage } from '../../../../../legacy/plugins/ml/common/types/audit_message'; const SIZE = 50; diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/index.js b/x-pack/plugins/ml/server/models/data_frame_analytics/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_frame_analytics/index.js rename to x-pack/plugins/ml/server/models/data_frame_analytics/index.js diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts rename to x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts index de23950e5cc1c8..c51f65714bc055 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts @@ -5,7 +5,7 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { Module } from '../../../common/types/modules'; +import { Module } from '../../../../../legacy/plugins/ml/common/types/modules'; import { DataRecognizer } from '../data_recognizer'; describe('ML - data recognizer', () => { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts rename to x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 553de75e38e05d..8d2a6c9955da36 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -10,7 +10,7 @@ import numeral from '@elastic/numeral'; import { CallAPIOptions, RequestHandlerContext, SavedObjectsClientContract } from 'kibana/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; -import { MlJob } from '../../../common/types/jobs'; +import { MlJob } from '../../../../../legacy/plugins/ml/common/types/jobs'; import { KibanaObjects, ModuleDataFeed, @@ -23,8 +23,11 @@ import { JobResponse, KibanaObjectResponse, DataRecognizerConfigResponse, -} from '../../../common/types/modules'; -import { getLatestDataOrBucketTimestamp, prefixDatafeedId } from '../../../common/util/job_utils'; +} from '../../../../../legacy/plugins/ml/common/types/modules'; +import { + getLatestDataOrBucketTimestamp, + prefixDatafeedId, +} from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { mlLog } from '../../client/log'; // @ts-ignore import { jobServiceProvider } from '../job_service'; diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/index.ts b/x-pack/plugins/ml/server/models/data_recognizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/index.ts rename to x-pack/plugins/ml/server/models/data_recognizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/data_visualizer/data_visualizer.ts rename to x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index b0a61b1232dc0b..9463f74e1e746e 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -6,8 +6,8 @@ import { CallAPIOptions, IScopedClusterClient } from 'src/core/server'; import _ from 'lodash'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { getSafeAggregationName } from '../../../common/util/job_utils'; +import { ML_JOB_FIELD_TYPES } from '../../../../../legacy/plugins/ml/common/constants/field_types'; +import { getSafeAggregationName } from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { buildBaseFilterCriteria, buildSamplerAggregation, diff --git a/x-pack/legacy/plugins/ml/server/models/data_visualizer/index.ts b/x-pack/plugins/ml/server/models/data_visualizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_visualizer/index.ts rename to x-pack/plugins/ml/server/models/data_visualizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts rename to x-pack/plugins/ml/server/models/fields_service/fields_service.d.ts diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js b/x-pack/plugins/ml/server/models/fields_service/fields_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js rename to x-pack/plugins/ml/server/models/fields_service/fields_service.js diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/index.ts b/x-pack/plugins/ml/server/models/fields_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/index.ts rename to x-pack/plugins/ml/server/models/fields_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts rename to x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 9f30f609c60b63..1d0452f2337f95 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'kibana/server'; -import { FindFileStructureResponse } from '../../../common/types/file_datavisualizer'; +import { FindFileStructureResponse } from '../../../../../legacy/plugins/ml/common/types/file_datavisualizer'; export type InputData = any[]; diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts rename to x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts index 008efb43a6c07e..e4de71ad0793d2 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts @@ -5,7 +5,7 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; +import { INDEX_META_DATA_CREATED_BY } from '../../../../../legacy/plugins/ml/common/constants/file_datavisualizer'; import { InputData } from './file_data_visualizer'; export interface Settings { diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts rename to x-pack/plugins/ml/server/models/file_data_visualizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/filter/filter_manager.ts rename to x-pack/plugins/ml/server/models/filter/filter_manager.ts index f40663a5eb6b2d..baba495257acac 100644 --- a/x-pack/legacy/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -7,7 +7,10 @@ import Boom from 'boom'; import { IScopedClusterClient } from 'src/core/server'; -import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; +import { + DetectorRule, + DetectorRuleScope, +} from '../../../../../legacy/plugins/ml/common/types/detector_rules'; export interface Filter { filter_id: string; diff --git a/x-pack/legacy/plugins/ml/server/models/filter/index.js b/x-pack/plugins/ml/server/models/filter/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/filter/index.js rename to x-pack/plugins/ml/server/models/filter/index.js diff --git a/x-pack/legacy/plugins/ml/server/models/filter/index.ts b/x-pack/plugins/ml/server/models/filter/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/filter/index.ts rename to x-pack/plugins/ml/server/models/filter/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/index.ts b/x-pack/plugins/ml/server/models/job_audit_messages/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_audit_messages/index.ts rename to x-pack/plugins/ml/server/models/job_audit_messages/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts rename to x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js rename to x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 2cdfc0ef4f4c5f..b434846d6f0f4c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; import moment from 'moment'; const SIZE = 1000; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/datafeeds.js b/x-pack/plugins/ml/server/models/job_service/datafeeds.js similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/job_service/datafeeds.js rename to x-pack/plugins/ml/server/models/job_service/datafeeds.js index c3b54fff0682d8..961b712610512d 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/datafeeds.js +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.js @@ -5,7 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + JOB_STATE, + DATAFEED_STATE, +} from '../../../../../legacy/plugins/ml/common/constants/states'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; export function datafeedsProvider(callWithRequest) { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/error_utils.js b/x-pack/plugins/ml/server/models/job_service/error_utils.js similarity index 94% rename from x-pack/legacy/plugins/ml/server/models/job_service/error_utils.js rename to x-pack/plugins/ml/server/models/job_service/error_utils.js index 6f25b5870f85ce..21e45110e70930 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/error_utils.js +++ b/x-pack/plugins/ml/server/models/job_service/error_utils.js @@ -5,7 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + JOB_STATE, + DATAFEED_STATE, +} from '../../../../../legacy/plugins/ml/common/constants/states'; const REQUEST_TIMEOUT = 'RequestTimeout'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/plugins/ml/server/models/job_service/groups.js similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/job_service/groups.js rename to x-pack/plugins/ml/server/models/job_service/groups.js index 6fbc071ef9854f..b30e9cdc6048b7 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/plugins/ml/server/models/job_service/groups.js @@ -5,7 +5,7 @@ */ import { CalendarManager } from '../calendar'; -import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; +import { GLOBAL_CALENDAR } from '../../../../../legacy/plugins/ml/common/constants/calendars'; export function groupsProvider(callWithRequest) { const calMngr = new CalendarManager(callWithRequest); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/plugins/ml/server/models/job_service/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/index.js rename to x-pack/plugins/ml/server/models/job_service/index.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/plugins/ml/server/models/job_service/jobs.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_service/jobs.js rename to x-pack/plugins/ml/server/models/job_service/jobs.js index b4b476c1f926ea..16d3c30bb0a280 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/plugins/ml/server/models/job_service/jobs.js @@ -5,7 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + JOB_STATE, + DATAFEED_STATE, +} from '../../../../../legacy/plugins/ml/common/constants/states'; import { datafeedsProvider } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; import { resultsServiceProvider } from '../results_service'; @@ -14,7 +17,7 @@ import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, -} from '../../../common/util/job_utils'; +} from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { groupsProvider } from './groups'; import { uniq } from 'lodash'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index ea2c71b04f56d2..1a098fdf16bb7c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -6,13 +6,13 @@ import { chunk } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/new_job'; +import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../../../legacy/plugins/ml/common/constants/new_job'; import { Token, CategorizationAnalyzer, CategoryFieldExample, -} from '../../../../../common/types/categories'; -import { callWithRequestType } from '../../../../../common/types/kibana'; +} from '../../../../../../../legacy/plugins/ml/common/types/categories'; +import { callWithRequestType } from '../../../../../../../legacy/plugins/ml/common/types/kibana'; import { ValidationResults } from './validation_results'; const CHUNK_SIZE = 100; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts similarity index 92% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 3361cc454e2b7b..c8eb0002a31c80 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -5,9 +5,12 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; -import { CategoryId, Category } from '../../../../../common/types/categories'; -import { callWithRequestType } from '../../../../../common/types/kibana'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { + CategoryId, + Category, +} from '../../../../../../../legacy/plugins/ml/common/types/categories'; +import { callWithRequestType } from '../../../../../../../legacy/plugins/ml/common/types/kibana'; export function topCategoriesProvider(callWithRequest: callWithRequestType) { async function getTotalCategories(jobId: string): Promise<{ total: number }> { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts index 34e63eabb405ef..bb1106b4d6396f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts @@ -9,13 +9,13 @@ import { CATEGORY_EXAMPLES_VALIDATION_STATUS, CATEGORY_EXAMPLES_ERROR_LIMIT, CATEGORY_EXAMPLES_WARNING_LIMIT, -} from '../../../../../common/constants/new_job'; +} from '../../../../../../../legacy/plugins/ml/common/constants/new_job'; import { FieldExampleCheck, CategoryFieldExample, VALIDATION_RESULT, -} from '../../../../../common/types/categories'; -import { getMedianStringLength } from '../../../../../common/util/string_utils'; +} from '../../../../../../../legacy/plugins/ml/common/types/categories'; +import { getMedianStringLength } from '../../../../../../../legacy/plugins/ml/common/util/string_utils'; const VALID_TOKEN_COUNT = 3; const MEDIAN_LINE_LENGTH_LIMIT = 400; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts similarity index 87% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/charts.ts index 88ae8caa91e4a1..e662e3ca03ded6 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts @@ -6,7 +6,7 @@ import { newJobLineChartProvider } from './line_chart'; import { newJobPopulationChartProvider } from './population_chart'; -import { callWithRequestType } from '../../../../common/types/kibana'; +import { callWithRequestType } from '../../../../../../legacy/plugins/ml/common/types/kibana'; export function newJobChartsProvider(callWithRequest: callWithRequestType) { const { newJobLineChart } = newJobLineChartProvider(callWithRequest); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts b/x-pack/plugins/ml/server/models/job_service/new_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts similarity index 92% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index c1a5ad5e38ecc9..3dfe935c655d5c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -5,9 +5,12 @@ */ import { get } from 'lodash'; -import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; -import { callWithRequestType } from '../../../../common/types/kibana'; -import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { + AggFieldNamePair, + EVENT_RATE_FIELD_ID, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { callWithRequestType } from '../../../../../../legacy/plugins/ml/common/types/kibana'; +import { ML_MEDIAN_PERCENTS } from '../../../../../../legacy/plugins/ml/common/util/job_utils'; type DtrIndex = number; type TimeStamp = number; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index ee35f13c44ee60..d1ef9773f8f179 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -5,9 +5,12 @@ */ import { get } from 'lodash'; -import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; -import { callWithRequestType } from '../../../../common/types/kibana'; -import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { + AggFieldNamePair, + EVENT_RATE_FIELD_ID, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { callWithRequestType } from '../../../../../../legacy/plugins/ml/common/types/kibana'; +import { ML_MEDIAN_PERCENTS } from '../../../../../../legacy/plugins/ml/common/util/job_utils'; const OVER_FIELD_EXAMPLES_COUNT = 40; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts index efe06f8b5ad4ab..475612f276c724 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Aggregation, METRIC_AGG_TYPE } from '../../../../common/types/fields'; +import { + Aggregation, + METRIC_AGG_TYPE, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, ES_AGGREGATION, -} from '../../../../common/constants/aggregation_types'; +} from '../../../../../../legacy/plugins/ml/common/constants/aggregation_types'; // aggregation object missing id, title and fields and has null for kibana and dsl aggregation names. // this is used as the basis for the ML only aggregations diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 5827201a636619..446c71dd40f684 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -12,9 +12,9 @@ import { FieldId, NewJobCaps, METRIC_AGG_TYPE, -} from '../../../../common/types/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/server'; -import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; +} from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/server'; +import { ML_JOB_AGGREGATION } from '../../../../../../legacy/plugins/ml/common/constants/aggregation_types'; import { rollupServiceProvider, RollupJob, RollupFields } from './rollup'; import { aggregations, mlOnlyAggregations } from './aggregations'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/index.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/index.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts similarity index 93% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index 3a9d979ccb22ca..0a967c760a1932 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -5,7 +5,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; +import { + Aggregation, + Field, + NewJobCaps, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; import { fieldServiceProvider } from './field_service'; interface NewJobCapsResponse { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts similarity index 92% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 1e9ce3d8d50225..4cbdfe4f360e03 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -7,8 +7,8 @@ import { SavedObject } from 'src/core/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { SavedObjectsClientContract } from 'kibana/server'; -import { FieldId } from '../../../../common/types/fields'; -import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { FieldId } from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { ES_AGGREGATION } from '../../../../../../legacy/plugins/ml/common/constants/aggregation_types'; export type RollupFields = Record]>; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js index 3dc2bee1e8705f..023e0f5b614ed8 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { validateBucketSpan } from '../validate_bucket_span'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../common/constants/validation'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../../../legacy/plugins/ml/common/constants/validation'; // farequote2017 snapshot snapshot mock search response // it returns a mock for the response of PolledDataChecker's search request diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/index.ts b/x-pack/plugins/ml/server/models/job_validation/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/index.ts rename to x-pack/plugins/ml/server/models/job_validation/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.d.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts similarity index 83% rename from x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.d.ts rename to x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts index 4580602b0af238..bb8a372eaba30c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts @@ -6,7 +6,7 @@ import { APICaller } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; -import { validateJobSchema } from '../../new_platform/job_validation_schema'; +import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; type ValidateJobPayload = TypeOf; @@ -15,5 +15,5 @@ export function validateJob( payload: ValidateJobPayload, kbnVersion: string, callAsInternalUser: APICaller, - xpackMainPlugin: any + isSecurityDisabled: boolean ): string[]; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.js similarity index 94% rename from x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/job_validation.js index ab1fbb39ee7062..d453c9add97d11 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.js @@ -8,11 +8,14 @@ import { i18n } from '@kbn/i18n'; import Boom from 'boom'; import { fieldsServiceProvider } from '../fields_service'; -import { renderTemplate } from '../../../common/util/string_utils'; +import { renderTemplate } from '../../../../../legacy/plugins/ml/common/util/string_utils'; import { getMessages } from './messages'; -import { VALIDATION_STATUS } from '../../../common/constants/validation'; +import { VALIDATION_STATUS } from '../../../../../legacy/plugins/ml/common/constants/validation'; -import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_utils'; +import { + basicJobValidation, + uniqWithIsEqual, +} from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; @@ -24,7 +27,7 @@ export async function validateJob( payload, kbnVersion = 'current', callAsInternalUser, - xpackMainPlugin + isSecurityDisabled ) { const messages = getMessages(); @@ -112,7 +115,7 @@ export async function validateJob( job, duration, callAsInternalUser, - xpackMainPlugin + isSecurityDisabled )) ); validationMessages.push(...(await validateTimeRange(callWithRequest, job, duration))); diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/messages.js b/x-pack/plugins/ml/server/models/job_validation/messages.js similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/job_validation/messages.js rename to x-pack/plugins/ml/server/models/job_validation/messages.js index 2c0c218bf86b51..33931f03facc3a 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/messages.js +++ b/x-pack/plugins/ml/server/models/job_validation/messages.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_ID_MAX_LENGTH } from '../../../common/constants/validation'; +import { JOB_ID_MAX_LENGTH } from '../../../../../legacy/plugins/ml/common/constants/validation'; let messages; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js similarity index 93% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_bucket_span.js rename to x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index 2914f086c1a83e..9e96e2219fb0f5 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -5,9 +5,9 @@ */ import { estimateBucketSpanFactory } from '../../models/bucket_span_estimator'; -import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { mlFunctionToESAggregation } from '../../../../../legacy/plugins/ml/common/util/job_utils'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../../legacy/plugins/ml/common/constants/validation'; +import { parseInterval } from '../../../../../legacy/plugins/ml/common/util/parse_interval'; import { validateJobObject } from './validate_job_object'; @@ -51,7 +51,7 @@ export async function validateBucketSpan( job, duration, callAsInternalUser, - xpackMainPlugin + isSecurityDisabled ) { validateJobObject(job); @@ -124,7 +124,7 @@ export async function validateBucketSpan( estimateBucketSpanFactory( callWithRequest, callAsInternalUser, - xpackMainPlugin + isSecurityDisabled )(data) .then(resolve) // this catch gets triggered when the estimation code runs without error diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.d.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts similarity index 78% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.d.ts rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts index dc109055337889..d3930ecf44c8d5 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts @@ -5,7 +5,10 @@ */ import { APICaller } from 'src/core/server'; -import { Job, Datafeed } from '../../../public/application/jobs/new_job/common/job_creator/configs'; +import { + Job, + Datafeed, +} from '../../../../../legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs'; interface ValidateCardinalityConfig extends Job { datafeed_config?: Datafeed; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_job_object.js b/x-pack/plugins/ml/server/models/job_validation/validate_job_object.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_job_object.js rename to x-pack/plugins/ml/server/models/job_validation/validate_job_object.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_model_memory_limit.js b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_model_memory_limit.js rename to x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.js index 733ed9c3c22c67..354a3124a534f7 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_model_memory_limit.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.js @@ -7,7 +7,7 @@ import numeral from '@elastic/numeral'; import { validateJobObject } from './validate_job_object'; import { calculateModelMemoryLimitProvider } from '../../models/calculate_model_memory_limit'; -import { ALLOWED_DATA_UNITS } from '../../../common/constants/validation'; +import { ALLOWED_DATA_UNITS } from '../../../../../legacy/plugins/ml/common/constants/validation'; // The minimum value the backend expects is 1MByte const MODEL_MEMORY_LIMIT_MINIMUM_BYTES = 1048576; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.js similarity index 93% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_time_range.js rename to x-pack/plugins/ml/server/models/job_validation/validate_time_range.js index df14d372664961..e6a92b45649b0d 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_time_range.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.js @@ -6,8 +6,8 @@ import _ from 'lodash'; -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/server'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/server'; +import { parseInterval } from '../../../../../legacy/plugins/ml/common/util/parse_interval'; import { validateJobObject } from './validate_job_object'; const BUCKET_SPAN_COMPARE_FACTOR = 25; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts rename to x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts index 2bd19985c85189..f2d74fb9152996 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts +++ b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnomalyRecordDoc } from '../../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../../../legacy/plugins/ml/common/types/anomalies'; export interface AnomaliesTableRecord { time: number; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.js b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.js rename to x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js index 4934a0ba07081e..fc4280c74994d9 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.js +++ b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js @@ -12,7 +12,7 @@ import { getEntityFieldValue, showActualForFunction, showTypicalForFunction, -} from '../../../common/util/anomaly_utils'; +} from '../../../../../legacy/plugins/ml/common/util/anomaly_utils'; // Builds the items for display in the anomalies table from the supplied list of anomaly records. // Provide the timezone to use for aggregating anomalies (by day or hour) as set in the diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts rename to x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 99eeaacc8de9cd..5d536059cb0a27 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -5,8 +5,8 @@ */ import Boom from 'boom'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { callWithRequestType } from '../../../common/types/kibana'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { callWithRequestType } from '../../../../../legacy/plugins/ml/common/types/kibana'; import { CriteriaField } from './results_service'; const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/index.ts b/x-pack/plugins/ml/server/models/results_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/results_service/index.ts rename to x-pack/plugins/ml/server/models/results_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts rename to x-pack/plugins/ml/server/models/results_service/results_service.ts index 555a58fbb53335..324cbb91ca8c10 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -9,10 +9,10 @@ import moment from 'moment'; import { SearchResponse } from 'elasticsearch'; import { RequestHandlerContext } from 'kibana/server'; import { buildAnomalyTableItems, AnomaliesTableRecord } from './build_anomaly_table_items'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../legacy/plugins/ml/common/constants/search'; import { getPartitionFieldsValuesFactory } from './get_partition_fields_values'; -import { AnomalyRecordDoc } from '../../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../../../legacy/plugins/ml/common/types/anomalies'; // Service for carrying out Elasticsearch queries to obtain data for the // ML Results dashboards. diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts new file mode 100644 index 00000000000000..b5adf1fedec791 --- /dev/null +++ b/x-pack/plugins/ml/server/plugin.ts @@ -0,0 +1,168 @@ +/* + * 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 { CoreSetup, IScopedClusterClient, Logger, PluginInitializerContext } from 'src/core/server'; +import { LicenseCheckResult, PluginsSetup, RouteInitialization } from './types'; +import { PLUGIN_ID } from '../../../legacy/plugins/ml/common/constants/app'; +import { VALID_FULL_LICENSE_MODES } from '../../../legacy/plugins/ml/common/constants/license'; + +// @ts-ignore: could not find declaration file for module +import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; +import { makeMlUsageCollector } from './lib/ml_telemetry'; +import { initMlServerLog } from './client/log'; +import { addLinksToSampleDatasets } from './lib/sample_data_sets'; + +import { annotationRoutes } from './routes/annotations'; +import { calendars } from './routes/calendars'; +import { dataFeedRoutes } from './routes/datafeeds'; +import { dataFrameAnalyticsRoutes } from './routes/data_frame_analytics'; +import { dataRecognizer } from './routes/modules'; +import { dataVisualizerRoutes } from './routes/data_visualizer'; +import { fieldsService } from './routes/fields_service'; +import { fileDataVisualizerRoutes } from './routes/file_data_visualizer'; +import { filtersRoutes } from './routes/filters'; +import { indicesRoutes } from './routes/indices'; +import { jobAuditMessagesRoutes } from './routes/job_audit_messages'; +import { jobRoutes } from './routes/anomaly_detectors'; +import { jobServiceRoutes } from './routes/job_service'; +import { jobValidationRoutes } from './routes/job_validation'; +import { notificationRoutes } from './routes/notification_settings'; +import { resultsServiceRoutes } from './routes/results_service'; +import { systemRoutes } from './routes/system'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + ml?: { + mlClient: IScopedClusterClient; + }; + } +} + +export class MlServerPlugin { + private readonly pluginId: string = PLUGIN_ID; + private log: Logger; + private version: string; + + private licenseCheckResults: LicenseCheckResult = { + isAvailable: false, + isActive: false, + isEnabled: false, + isSecurityDisabled: false, + }; + + constructor(ctx: PluginInitializerContext) { + this.log = ctx.logger.get(); + this.version = ctx.env.packageInfo.branch; + } + + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + let sampleLinksInitialized = false; + + plugins.features.registerFeature({ + id: PLUGIN_ID, + name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { + defaultMessage: 'Machine Learning', + }), + icon: 'machineLearningApp', + navLinkId: PLUGIN_ID, + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + description: i18n.translate('xpack.ml.feature.reserved.description', { + defaultMessage: + 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.', + }), + }, + }); + + // Can access via router's handler function 'context' parameter - context.ml.mlClient + const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { + plugins: [elasticsearchJsPlugin], + }); + + coreSetup.http.registerRouteHandlerContext(PLUGIN_ID, (context, request) => { + return { + mlClient: mlClient.asScoped(request), + }; + }); + + const routeInit: RouteInitialization = { + router: coreSetup.http.createRouter(), + getLicenseCheckResults: () => this.licenseCheckResults, + }; + + annotationRoutes(routeInit, plugins.security); + calendars(routeInit); + dataFeedRoutes(routeInit); + dataFrameAnalyticsRoutes(routeInit); + dataRecognizer(routeInit); + dataVisualizerRoutes(routeInit); + fieldsService(routeInit); + fileDataVisualizerRoutes(routeInit); + filtersRoutes(routeInit); + indicesRoutes(routeInit); + jobAuditMessagesRoutes(routeInit); + jobRoutes(routeInit); + jobServiceRoutes(routeInit); + notificationRoutes(routeInit); + resultsServiceRoutes(routeInit); + jobValidationRoutes(routeInit, this.version); + systemRoutes(routeInit, { + spacesPlugin: plugins.spaces, + cloud: plugins.cloud, + }); + initMlServerLog({ log: this.log }); + coreSetup.getStartServices().then(([core]) => { + makeMlUsageCollector(plugins.usageCollection, core.savedObjects); + }); + + plugins.licensing.license$.subscribe(async license => { + const { isEnabled: securityIsEnabled } = license.getFeature('security'); + // @ts-ignore isAvailable is not read + const { isAvailable, isEnabled } = license.getFeature(this.pluginId); + + this.licenseCheckResults = { + isActive: license.isActive, + // This `isAvailable` check for the ml plugin returns false for a basic license + // ML should be available on basic with reduced functionality (only file data visualizer) + // TODO: This will need to be updated in the second step of this cutover to NP. + isAvailable: isEnabled, + isEnabled, + isSecurityDisabled: securityIsEnabled === false, + type: license.type, + }; + + if (sampleLinksInitialized === false) { + sampleLinksInitialized = true; + // Add links to the Kibana sample data sets if ml is enabled + // and license is trial or platinum. + if (isEnabled === true && plugins.home) { + if ( + this.licenseCheckResults.type && + VALID_FULL_LICENSE_MODES.includes(this.licenseCheckResults.type) + ) { + addLinksToSampleDatasets({ + addAppLinksToSampleDataset: plugins.home.sampleData.addAppLinksToSampleDataset, + }); + } + } + } + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/legacy/plugins/ml/server/routes/README.md b/x-pack/plugins/ml/server/routes/README.md similarity index 100% rename from x-pack/legacy/plugins/ml/server/routes/README.md rename to x-pack/plugins/ml/server/routes/README.md diff --git a/x-pack/legacy/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts similarity index 83% rename from x-pack/legacy/plugins/ml/server/routes/annotations.ts rename to x-pack/plugins/ml/server/routes/annotations.ts index 20f52b4b051c4c..bcc0238c366a3d 100644 --- a/x-pack/legacy/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -9,18 +9,19 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; +import { SecurityPluginSetup } from '../../../security/server'; import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; import { annotationServiceProvider } from '../models/annotation_service'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { deleteAnnotationSchema, getAnnotationsSchema, indexAnnotationSchema, -} from '../new_platform/annotations_schema'; +} from './schemas/annotations_schema'; -import { ANNOTATION_USER_UNKNOWN } from '../../common/constants/annotations'; +import { ANNOTATION_USER_UNKNOWN } from '../../../../legacy/plugins/ml/common/constants/annotations'; function getAnnotationsFeatureUnavailableErrorMessage() { return Boom.badRequest( @@ -34,7 +35,10 @@ function getAnnotationsFeatureUnavailableErrorMessage() { /** * Routes for annotations */ -export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: RouteInitialization) { +export function annotationRoutes( + { router, getLicenseCheckResults }: RouteInitialization, + securityPlugin: SecurityPluginSetup +) { /** * @apiGroup Annotations * @@ -57,7 +61,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro body: schema.object(getAnnotationsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getAnnotations } = annotationServiceProvider(context); const resp = await getAnnotations(request.body); @@ -88,7 +92,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro body: schema.object(indexAnnotationSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( context.ml!.mlClient.callAsCurrentUser @@ -99,6 +103,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro const { indexAnnotation } = annotationServiceProvider(context); const user = securityPlugin.authc.getCurrentUser(request) || {}; + // @ts-ignore username doesn't exist on {} const resp = await indexAnnotation(request.body, user.username || ANNOTATION_USER_UNKNOWN); return response.ok({ @@ -126,7 +131,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro params: schema.object(deleteAnnotationSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( context.ml!.mlClient.callAsCurrentUser diff --git a/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts rename to x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 99dbdec9e945bd..7bf2fb7bc6903d 100644 --- a/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -6,17 +6,17 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, anomalyDetectionUpdateJobSchema, -} from '../new_platform/anomaly_detectors_schema'; +} from './schemas/anomaly_detectors_schema'; /** * Routes for the anomaly detectors */ -export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function jobRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * @@ -32,7 +32,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { path: '/api/ml/anomaly_detectors', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobs'); return response.ok({ @@ -62,7 +62,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobs', { jobId }); @@ -90,7 +90,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { path: '/api/ml/anomaly_detectors/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobStats'); return response.ok({ @@ -120,7 +120,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobStats', { jobId }); @@ -152,7 +152,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...anomalyDetectionJobSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.addJob', { @@ -187,7 +187,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...anomalyDetectionUpdateJobSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.updateJob', { @@ -221,7 +221,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.openJob', { @@ -254,7 +254,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { jobId: string; force?: boolean } = { jobId: request.params.jobId, @@ -291,7 +291,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { jobId: string; force?: boolean } = { jobId: request.params.jobId, @@ -326,7 +326,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.any(), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.validateDetector', { body: request.body, @@ -359,7 +359,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ duration: schema.any() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const jobId = request.params.jobId; const duration = request.body.duration; @@ -407,7 +407,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.records', { jobId: request.params.jobId, @@ -456,7 +456,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.buckets', { jobId: request.params.jobId, @@ -499,7 +499,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.overallBuckets', { jobId: request.params.jobId, @@ -537,7 +537,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options = { jobId: request.params.jobId, diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/routes/apidoc.json rename to x-pack/plugins/ml/server/routes/apidoc.json diff --git a/x-pack/legacy/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts similarity index 84% rename from x-pack/legacy/plugins/ml/server/routes/calendars.ts rename to x-pack/plugins/ml/server/routes/calendars.ts index 8e4e1c4c14751d..ae494d3578890b 100644 --- a/x-pack/legacy/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -6,10 +6,10 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { calendarSchema } from '../new_platform/calendars_schema'; +import { RouteInitialization } from '../types'; +import { calendarSchema } from './schemas/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { @@ -42,13 +42,13 @@ function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) return cal.getCalendarsByIds(calendarIds); } -export function calendars({ xpackMainPlugin, router }: RouteInitialization) { +export function calendars({ router, getLicenseCheckResults }: RouteInitialization) { router.get( { path: '/api/ml/calendars', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAllCalendars(context); @@ -68,7 +68,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { params: schema.object({ calendarIds: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { let returnValue; try { const calendarIds = request.params.calendarIds.split(','); @@ -95,7 +95,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...calendarSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const body = request.body; const resp = await newCalendar(context, body); @@ -117,7 +117,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...calendarSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { calendarId } = request.params; const body = request.body; @@ -139,7 +139,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { params: schema.object({ calendarId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { calendarId } = request.params; const resp = await deleteCalendar(context, calendarId); diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts rename to x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 6541fa541a59ff..0a93320c05eb5b 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -7,18 +7,18 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { dataAnalyticsJobConfigSchema, dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, -} from '../new_platform/data_analytics_schema'; +} from './schemas/data_analytics_schema'; /** * Routes for the data frame analytics */ -export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DataFrameAnalytics * @@ -36,7 +36,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.maybe(schema.string()) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics'); return response.ok({ @@ -64,7 +64,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics', { @@ -91,7 +91,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti path: '/api/ml/data_frame/analytics/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.getDataFrameAnalyticsStats' @@ -121,7 +121,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -159,7 +159,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti body: schema.object(dataAnalyticsJobConfigSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -192,7 +192,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti body: schema.object({ ...dataAnalyticsEvaluateSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.evaluateDataFrameAnalytics', @@ -232,7 +232,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti body: schema.object({ ...dataAnalyticsExplainSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.explainDataFrameAnalytics', @@ -267,7 +267,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -303,7 +303,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.startDataFrameAnalytics', { @@ -337,7 +337,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { analyticsId: string; force?: boolean | undefined } = { analyticsId: request.params.analyticsId, @@ -377,7 +377,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider( diff --git a/x-pack/legacy/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/routes/data_visualizer.ts rename to x-pack/plugins/ml/server/routes/data_visualizer.ts index df7e4b70108777..e4d068784def1b 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -11,9 +11,9 @@ import { Field } from '../models/data_visualizer/data_visualizer'; import { dataVisualizerFieldStatsSchema, dataVisualizerOverallStatsSchema, -} from '../new_platform/data_visualizer_schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +} from './schemas/data_visualizer_schema'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; function getOverallStats( context: RequestHandlerContext, @@ -68,7 +68,7 @@ function getStatsForFields( /** * Routes for the index data visualizer. */ -export function dataVisualizerRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function dataVisualizerRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DataVisualizer * @@ -83,7 +83,7 @@ export function dataVisualizerRoutes({ xpackMainPlugin, router }: RouteInitializ path: '/api/ml/data_visualizer/get_field_stats/{indexPatternTitle}', validate: dataVisualizerFieldStatsSchema, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { params: { indexPatternTitle }, @@ -135,7 +135,7 @@ export function dataVisualizerRoutes({ xpackMainPlugin, router }: RouteInitializ path: '/api/ml/data_visualizer/get_overall_stats/{indexPatternTitle}', validate: dataVisualizerOverallStatsSchema, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { params: { indexPatternTitle }, diff --git a/x-pack/legacy/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts similarity index 86% rename from x-pack/legacy/plugins/ml/server/routes/datafeeds.ts rename to x-pack/plugins/ml/server/routes/datafeeds.ts index 9335403616cf7d..e3bce4c1328e4e 100644 --- a/x-pack/legacy/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -5,15 +5,15 @@ */ import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { startDatafeedSchema, datafeedConfigSchema } from '../new_platform/datafeeds_schema'; +import { RouteInitialization } from '../types'; +import { startDatafeedSchema, datafeedConfigSchema } from './schemas/datafeeds_schema'; /** * Routes for datafeed service */ -export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DatafeedService * @@ -26,7 +26,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/datafeeds', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeeds'); @@ -53,7 +53,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeeds', { datafeedId }); @@ -79,7 +79,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/datafeeds/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedStats'); @@ -106,7 +106,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedStats', { @@ -137,7 +137,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) body: datafeedConfigSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.addDatafeed', { @@ -169,7 +169,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) body: datafeedConfigSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.updateDatafeed', { @@ -201,7 +201,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) query: schema.maybe(schema.object({ force: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { datafeedId: string; force?: boolean } = { datafeedId: request.params.jobId, @@ -237,7 +237,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) body: startDatafeedSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const { start, end } = request.body; @@ -271,7 +271,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; @@ -302,7 +302,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedPreview', { diff --git a/x-pack/legacy/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts similarity index 84% rename from x-pack/legacy/plugins/ml/server/routes/fields_service.ts rename to x-pack/plugins/ml/server/routes/fields_service.ts index 4827adf23d7b46..bc092190c2c620 100644 --- a/x-pack/legacy/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -5,13 +5,13 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { getCardinalityOfFieldsSchema, getTimeFieldRangeSchema, -} from '../new_platform/fields_service_schema'; +} from './schemas/fields_service_schema'; import { fieldsServiceProvider } from '../models/fields_service'; function getCardinalityOfFields(context: RequestHandlerContext, payload: any) { @@ -29,7 +29,7 @@ function getTimeFieldRange(context: RequestHandlerContext, payload: any) { /** * Routes for fields service */ -export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) { +export function fieldsService({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup FieldsService * @@ -44,7 +44,7 @@ export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) body: getCardinalityOfFieldsSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -71,7 +71,7 @@ export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) body: getTimeFieldRangeSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getTimeFieldRange(context, request.body); diff --git a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts similarity index 87% rename from x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts rename to x-pack/plugins/ml/server/routes/file_data_visualizer.ts index d5a992c9332930..1d724a8843350d 100644 --- a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; -import { MAX_BYTES } from '../../common/constants/file_datavisualizer'; +import { MAX_BYTES } from '../../../../legacy/plugins/ml/common/constants/file_datavisualizer'; import { wrapError } from '../client/error_wrapper'; import { InputOverrides, @@ -18,8 +18,8 @@ import { Mappings, } from '../models/file_data_visualizer'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry'; function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { @@ -43,12 +43,7 @@ function importData( /** * Routes for the file data visualizer. */ -export function fileDataVisualizerRoutes({ - router, - xpackMainPlugin, - savedObjects, - elasticsearchPlugin, -}: RouteInitialization) { +export function fileDataVisualizerRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup FileDataVisualizer * @@ -87,7 +82,7 @@ export function fileDataVisualizerRoutes({ }, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const result = await analyzeFiles(context, request.body, request.query); return response.ok({ body: result }); @@ -129,7 +124,7 @@ export function fileDataVisualizerRoutes({ }, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { id } = request.query; const { index, data, settings, mappings, ingestPipeline } = request.body; @@ -138,7 +133,8 @@ export function fileDataVisualizerRoutes({ // 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. if (id === undefined) { - await incrementFileDataVisualizerIndexCreationCount(savedObjects!); + // @ts-ignore + await incrementFileDataVisualizerIndexCreationCount(context.core.savedObjects.client); } const result = await importData( diff --git a/x-pack/legacy/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts similarity index 87% rename from x-pack/legacy/plugins/ml/server/routes/filters.ts rename to x-pack/plugins/ml/server/routes/filters.ts index a06f8d4f8b727b..d5530668b26062 100644 --- a/x-pack/legacy/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -6,10 +6,10 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { createFilterSchema, updateFilterSchema } from '../new_platform/filters_schema'; +import { RouteInitialization } from '../types'; +import { createFilterSchema, updateFilterSchema } from './schemas/filters_schema'; import { FilterManager, FormFilter } from '../models/filter'; // TODO - add function for returning a list of just the filter IDs. @@ -44,7 +44,7 @@ function deleteFilter(context: RequestHandlerContext, filterId: string) { return mgr.deleteFilter(filterId); } -export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup Filters * @@ -60,7 +60,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/filters', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAllFilters(context); @@ -90,7 +90,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ filterId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getFilter(context, request.params.filterId); return response.ok({ @@ -119,7 +119,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) body: schema.object(createFilterSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const body = request.body; const resp = await newFilter(context, body); @@ -151,7 +151,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) body: schema.object(updateFilterSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { filterId } = request.params; const body = request.body; @@ -182,7 +182,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ filterId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { filterId } = request.params; const resp = await deleteFilter(context, filterId); @@ -212,7 +212,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/filters/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAllFilterStats(context); diff --git a/x-pack/legacy/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts similarity index 80% rename from x-pack/legacy/plugins/ml/server/routes/indices.ts rename to x-pack/plugins/ml/server/routes/indices.ts index 0ee15f1321e9c2..e01a7a0cbad287 100644 --- a/x-pack/legacy/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -6,13 +6,13 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; /** * Indices routes. */ -export function indicesRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function indicesRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup Indices * @@ -30,7 +30,7 @@ export function indicesRoutes({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { body: { index, fields: requestFields }, diff --git a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts similarity index 84% rename from x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts rename to x-pack/plugins/ml/server/routes/job_audit_messages.ts index 76986b935b993a..38df28e17ec0d6 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -5,15 +5,15 @@ */ import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; /** * Routes for job audit message routes */ -export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function jobAuditMessagesRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup JobAuditMessages * @@ -29,7 +29,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getJobAuditMessages } = jobAuditMessagesProvider( context.ml!.mlClient.callAsCurrentUser @@ -62,7 +62,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getJobAuditMessages } = jobAuditMessagesProvider( context.ml!.mlClient.callAsCurrentUser diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/job_service.ts rename to x-pack/plugins/ml/server/routes/job_service.ts index 5ddbd4cdfd5a5d..e15888088d3a13 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -7,10 +7,9 @@ import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { IScopedClusterClient } from 'src/core/server'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { isSecurityDisabled } from '../lib/security_utils'; +import { RouteInitialization } from '../types'; import { categorizationFieldExamplesSchema, chartSchema, @@ -21,7 +20,7 @@ import { lookBackProgressSchema, topCategoriesSchema, updateGroupsSchema, -} from '../new_platform/job_service_schema'; +} from './schemas/job_service_schema'; // @ts-ignore no declaration module import { jobServiceProvider } from '../models/job_service'; import { categorizationExamplesProvider } from '../models/job_service/new_job'; @@ -29,11 +28,12 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitialization) { async function hasPermissionToCreateJobs( callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] ) { - if (isSecurityDisabled(xpackMainPlugin) === true) { + const { isSecurityDisabled } = getLicenseCheckResults(); + if (isSecurityDisabled === true) { return true; } @@ -63,7 +63,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(forceStartDatafeedSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { datafeedIds, start, end } = request.body; @@ -92,7 +92,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(datafeedIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { datafeedIds } = request.body; @@ -121,7 +121,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -150,7 +150,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -179,7 +179,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -208,7 +208,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobsWithTimerangeSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { dateFormatTz } = request.body; @@ -237,7 +237,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -264,7 +264,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio path: '/api/ml/jobs/groups', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await getAllGroups(); @@ -292,7 +292,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(updateGroupsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobs } = request.body; @@ -319,7 +319,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await deletingJobTasks(); @@ -347,7 +347,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -377,7 +377,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPattern } = request.params; const isRollup = request.query.rollup === 'true'; @@ -408,7 +408,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(chartSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPatternTitle, @@ -461,7 +461,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(chartSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPatternTitle, @@ -509,7 +509,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await getAllJobAndGroupIds(); @@ -537,7 +537,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(lookBackProgressSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobId, start, end } = request.body; @@ -566,7 +566,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(categorizationFieldExamplesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { // due to the use of the _analyze endpoint which is called by the kibana user, // basic job creation privileges are required to use this endpoint @@ -625,7 +625,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(topCategoriesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobId, count } = request.body; diff --git a/x-pack/legacy/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts similarity index 85% rename from x-pack/legacy/plugins/ml/server/routes/job_validation.ts rename to x-pack/plugins/ml/server/routes/job_validation.ts index 64c9ccd27720a8..ae2e6885ba0f3b 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -7,15 +7,15 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'src/core/server'; import { schema, TypeOf } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { estimateBucketSpanSchema, modelMemoryLimitSchema, validateCardinalitySchema, validateJobSchema, -} from '../new_platform/job_validation_schema'; +} from './schemas/job_validation_schema'; import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; import { validateJob, validateCardinality } from '../models/job_validation'; @@ -25,7 +25,10 @@ type CalculateModelMemoryLimitPayload = TypeOf; /** * Routes for job validation */ -export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteInitialization) { +export function jobValidationRoutes( + { getLicenseCheckResults, router }: RouteInitialization, + version: string +) { function calculateModelMemoryLimit( context: RequestHandlerContext, payload: CalculateModelMemoryLimitPayload @@ -67,13 +70,13 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: estimateBucketSpanSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { let errorResp; const resp = await estimateBucketSpanFactory( context.ml!.mlClient.callAsCurrentUser, context.core.elasticsearch.adminClient.callAsInternalUser, - xpackMainPlugin + getLicenseCheckResults().isSecurityDisabled )(request.body) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. @@ -114,7 +117,7 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: modelMemoryLimitSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await calculateModelMemoryLimit(context, request.body); @@ -141,7 +144,7 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: schema.object(validateCardinalitySchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await validateCardinality( context.ml!.mlClient.callAsCurrentUser, @@ -171,16 +174,15 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: validateJobSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { - // pkg.branch corresponds to the version used in documentation links. - const version = config.get('pkg.branch'); + // version corresponds to the version used in documentation links. const resp = await validateJob( context.ml!.mlClient.callAsCurrentUser, request.body, version, context.core.elasticsearch.adminClient.callAsInternalUser, - xpackMainPlugin + getLicenseCheckResults().isSecurityDisabled ); return response.ok({ diff --git a/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts b/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts similarity index 71% rename from x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts rename to x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts index cc77d2872fb909..a371af1abf2d13 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts +++ b/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts @@ -10,10 +10,10 @@ import { RequestHandler, RequestHandlerContext, } from 'src/core/server'; -import { PLUGIN_ID, MlXpackMainPlugin } from './plugin'; +import { LicenseCheckResult } from '../types'; export const licensePreRoutingFactory = ( - xpackMainPlugin: MlXpackMainPlugin, + getLicenseCheckResults: () => LicenseCheckResult, handler: RequestHandler ): RequestHandler => { // License checking and enable/disable logic @@ -22,14 +22,10 @@ export const licensePreRoutingFactory = ( request: KibanaRequest, response: KibanaResponseFactory ) { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN_ID).getLicenseCheckResults(); + const licenseCheckResults = getLicenseCheckResults(); if (!licenseCheckResults.isAvailable) { - return response.forbidden({ - body: { - message: licenseCheckResults.message, - }, - }); + return response.forbidden(); } return handler(ctx, request, response); diff --git a/x-pack/legacy/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/modules.ts rename to x-pack/plugins/ml/server/routes/modules.ts index a40fb1c9149ca1..c9b005d4e43f92 100644 --- a/x-pack/legacy/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -6,12 +6,12 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; -import { DatafeedOverride, JobOverride } from '../../common/types/modules'; +import { DatafeedOverride, JobOverride } from '../../../../legacy/plugins/ml/common/types/modules'; import { wrapError } from '../client/error_wrapper'; import { DataRecognizer } from '../models/data_recognizer'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { getModuleIdParamSchema, setupModuleBodySchema } from '../new_platform/modules'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { getModuleIdParamSchema, setupModuleBodySchema } from './schemas/modules'; +import { RouteInitialization } from '../types'; function recognize(context: RequestHandlerContext, indexPatternTitle: string) { const dr = new DataRecognizer(context); @@ -65,7 +65,7 @@ function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: strin /** * Recognizer routes. */ -export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) { +export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DataRecognizer * @@ -84,7 +84,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPatternTitle } = request.params; const results = await recognize(context, indexPatternTitle); @@ -114,7 +114,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { let { moduleId } = request.params; if (moduleId === '') { @@ -150,7 +150,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) body: setupModuleBodySchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { moduleId } = request.params; @@ -207,7 +207,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { moduleId } = request.params; const result = await dataRecognizerJobsExist(context, moduleId); diff --git a/x-pack/legacy/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/routes/notification_settings.ts rename to x-pack/plugins/ml/server/routes/notification_settings.ts index c65627543b21d8..b68d2441333f93 100644 --- a/x-pack/legacy/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; /** * Routes for notification settings */ -export function notificationRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function notificationRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup NotificationSettings * @@ -24,7 +24,7 @@ export function notificationRoutes({ xpackMainPlugin, router }: RouteInitializat path: '/api/ml/notification_settings', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const params = { includeDefaults: true, diff --git a/x-pack/legacy/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/results_service.ts rename to x-pack/plugins/ml/server/routes/results_service.ts index 5d107b2d978090..77c998acc9f27a 100644 --- a/x-pack/legacy/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -6,16 +6,16 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { anomaliesTableDataSchema, categoryDefinitionSchema, categoryExamplesSchema, maxAnomalyScoreSchema, partitionFieldValuesSchema, -} from '../new_platform/results_service_schema'; +} from './schemas/results_service_schema'; import { resultsServiceProvider } from '../models/results_service'; function getAnomaliesTableData(context: RequestHandlerContext, payload: any) { @@ -74,7 +74,7 @@ function getPartitionFieldsValues(context: RequestHandlerContext, payload: any) /** * Routes for results service */ -export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup ResultsService * @@ -89,7 +89,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(anomaliesTableDataSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAnomaliesTableData(context, request.body); @@ -116,7 +116,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(categoryDefinitionSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getCategoryDefinition(context, request.body); @@ -143,7 +143,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(maxAnomalyScoreSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getMaxAnomalyScore(context, request.body); @@ -170,7 +170,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(categoryExamplesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getCategoryExamples(context, request.body); @@ -197,7 +197,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(partitionFieldValuesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getPartitionFieldsValues(context, request.body); diff --git a/x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.ts b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/calendars_schema.ts b/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/calendars_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/data_visualizer_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/datafeeds_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/filters_schema.ts b/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/filters_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/filters_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/modules.ts b/x-pack/plugins/ml/server/routes/schemas/modules.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/modules.ts rename to x-pack/plugins/ml/server/routes/schemas/modules.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/system.ts rename to x-pack/plugins/ml/server/routes/system.ts index 5861b53d74875c..36a9ea1447f583 100644 --- a/x-pack/legacy/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -11,20 +11,17 @@ import { RequestHandlerContext } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { mlLog } from '../client/log'; import { privilegesProvider } from '../lib/check_privileges'; -import { isSecurityDisabled } from '../lib/security_utils'; import { spacesUtilsProvider } from '../lib/spaces_utils'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization, SystemRouteDeps } from '../types'; /** * System routes */ -export function systemRoutes({ - router, - xpackMainPlugin, - spacesPlugin, - cloud, -}: RouteInitialization) { +export function systemRoutes( + { getLicenseCheckResults, router }: RouteInitialization, + { spacesPlugin, cloud }: SystemRouteDeps +) { async function getNodeCount(context: RequestHandlerContext) { const filterPath = 'nodes.*.attributes'; const resp = await context.ml!.mlClient.callAsInternalUser('nodes.info', { @@ -59,7 +56,7 @@ export function systemRoutes({ body: schema.maybe(schema.any()), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { let upgradeInProgress = false; try { @@ -80,7 +77,7 @@ export function systemRoutes({ } } - if (isSecurityDisabled(xpackMainPlugin)) { + if (getLicenseCheckResults().isSecurityDisabled) { // if xpack.security.enabled has been explicitly set to false // return that security is disabled and don't call the privilegeCheck endpoint return response.ok({ @@ -119,7 +116,7 @@ export function systemRoutes({ }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true @@ -130,7 +127,7 @@ export function systemRoutes({ const { getPrivileges } = privilegesProvider( context.ml!.mlClient.callAsCurrentUser, - xpackMainPlugin, + getLicenseCheckResults(), isMlEnabledInSpace, ignoreSpaces ); @@ -155,11 +152,11 @@ export function systemRoutes({ path: '/api/ml/ml_node_count', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { // check for basic license first for consistency with other // security disabled checks - if (isSecurityDisabled(xpackMainPlugin)) { + if (getLicenseCheckResults().isSecurityDisabled) { return response.ok({ body: await getNodeCount(context), }); @@ -206,7 +203,7 @@ export function systemRoutes({ path: '/api/ml/info', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const info = await context.ml!.mlClient.callAsCurrentUser('ml.info'); const cloudId = cloud && cloud.cloudId; @@ -234,7 +231,7 @@ export function systemRoutes({ body: schema.maybe(schema.any()), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { return response.ok({ body: await context.ml!.mlClient.callAsCurrentUser('search', request.body), diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts new file mode 100644 index 00000000000000..550abadb3c06f3 --- /dev/null +++ b/x-pack/plugins/ml/server/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { IRouter } from 'src/core/server'; +import { CloudSetup } from '../../cloud/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { SpacesPluginSetup } from '../../spaces/server'; + +export interface LicenseCheckResult { + isAvailable: boolean; + isActive: boolean; + isEnabled: boolean; + isSecurityDisabled: boolean; + status?: string; + type?: string; +} + +export interface SystemRouteDeps { + cloud: CloudSetup; + spacesPlugin: SpacesPluginSetup; +} + +export interface PluginsSetup { + cloud: CloudSetup; + features: FeaturesPluginSetup; + home: HomeServerPluginSetup; + licensing: LicensingPluginSetup; + security: SecurityPluginSetup; + spaces: SpacesPluginSetup; + usageCollection: UsageCollectionSetup; +} + +export interface RouteInitialization { + router: IRouter; + getLicenseCheckResults: () => LicenseCheckResult; +} From 0cede6a705db65ef450426db3c584fcabab42780 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 27 Feb 2020 19:17:07 -0700 Subject: [PATCH 20/34] Prep agg types for new platform (#57064) --- packages/kbn-utility-types/README.md | 1 + packages/kbn-utility-types/index.ts | 2 +- .../actions/filters/brush_event.test.ts | 49 +- src/legacy/core_plugins/data/public/index.ts | 12 +- src/legacy/core_plugins/data/public/plugin.ts | 6 +- .../public/search/aggs/agg_config.test.ts | 497 ++++++ .../data/public/search/aggs/agg_config.ts | 61 +- .../public/search/aggs/agg_configs.test.ts | 503 ++++++ .../data/public/search/aggs/agg_configs.ts | 76 +- .../public/search/aggs/agg_params.test.ts | 2 - .../data/public/search/aggs/agg_type.test.ts | 16 +- .../data/public/search/aggs/agg_type.ts | 5 +- .../search/aggs/agg_types_registry.test.ts | 91 ++ .../public/search/aggs/agg_types_registry.ts | 68 + .../search/aggs/buckets/_bucket_agg_type.ts | 12 +- .../search/aggs/buckets/_interval_options.ts | 1 + .../create_filter/date_histogram.test.ts | 12 +- .../buckets/create_filter/date_range.test.ts | 7 +- .../buckets/create_filter/filters.test.ts | 13 +- .../buckets/create_filter/histogram.test.ts | 12 +- .../buckets/create_filter/ip_range.test.ts | 11 +- .../aggs/buckets/create_filter/range.test.ts | 12 +- .../aggs/buckets/create_filter/terms.test.ts | 11 +- .../search/aggs/buckets/date_histogram.ts | 8 +- .../search/aggs/buckets/date_range.test.ts | 25 +- .../public/search/aggs/buckets/date_range.ts | 12 +- .../data/public/search/aggs/buckets/filter.ts | 1 + .../public/search/aggs/buckets/filters.ts | 26 +- .../search/aggs/buckets/geo_hash.test.ts | 7 +- .../public/search/aggs/buckets/geo_tile.ts | 3 +- .../search/aggs/buckets/histogram.test.ts | 33 +- .../public/search/aggs/buckets/histogram.ts | 12 +- .../public/search/aggs/buckets/ip_range.ts | 12 +- .../buckets/migrate_include_exclude_format.ts | 4 +- .../public/search/aggs/buckets/range.test.ts | 12 +- .../aggs/buckets/significant_terms.test.ts | 9 +- .../public/search/aggs/buckets/terms.test.ts | 8 +- .../aggs/filter/agg_type_filters.test.ts | 5 +- .../search/aggs/filter/agg_type_filters.ts | 1 + .../search/aggs/filter/prop_filter.test.ts | 19 +- .../data/public/search/aggs/index.test.ts | 2 - .../data/public/search/aggs/index.ts | 9 +- .../public/search/aggs/metrics/bucket_avg.ts | 1 - .../public/search/aggs/metrics/bucket_max.ts | 1 - .../public/search/aggs/metrics/bucket_min.ts | 1 + .../public/search/aggs/metrics/cardinality.ts | 5 +- .../data/public/search/aggs/metrics/count.ts | 7 +- .../lib/get_response_agg_config_class.ts | 1 + .../metrics/lib/parent_pipeline_agg_helper.ts | 1 - .../lib/sibling_pipeline_agg_helper.ts | 1 - .../public/search/aggs/metrics/median.test.ts | 7 +- .../data/public/search/aggs/metrics/median.ts | 4 +- .../search/aggs/metrics/metric_agg_type.ts | 7 +- .../data/public/search/aggs/metrics/min.ts | 1 + .../aggs/metrics/parent_pipeline.test.ts | 18 +- .../aggs/metrics/percentile_ranks.test.ts | 8 +- .../search/aggs/metrics/percentile_ranks.ts | 7 +- .../search/aggs/metrics/percentiles.test.ts | 6 +- .../public/search/aggs/metrics/percentiles.ts | 4 - .../aggs/metrics/sibling_pipeline.test.ts | 22 +- .../search/aggs/metrics/std_deviation.test.ts | 6 +- .../search/aggs/metrics/top_hit.test.ts | 6 +- .../public/search/aggs/param_types/agg.ts | 4 +- .../public/search/aggs/param_types/base.ts | 4 +- .../search/aggs/param_types/field.test.ts | 2 - .../public/search/aggs/param_types/field.ts | 5 +- .../param_types/filter/field_filters.test.ts | 11 +- .../aggs/param_types/filter/field_filters.ts | 8 +- .../search/aggs/param_types/json.test.ts | 8 +- .../public/search/aggs/param_types/json.ts | 4 +- .../search/aggs/param_types/optioned.test.ts | 2 - .../search/aggs/param_types/optioned.ts | 6 +- .../search/aggs/param_types/string.test.ts | 8 +- .../public/search/aggs/param_types/string.ts | 4 +- .../public/search/aggs/test_helpers/index.ts} | 4 +- .../test_helpers/mock_agg_types_registry.ts | 57 + .../aggs/test_helpers/mock_data_services.ts | 54 + .../data/public/search/aggs/types.ts | 2 +- .../data/public/search/aggs/utils.test.tsx | 2 - .../data/public/search/aggs/utils.ts | 39 +- .../data/public/search/expressions/esaggs.ts | 4 +- .../data/public/search/expressions/utils.ts | 5 +- .../core_plugins/data/public/search/mocks.ts | 85 + .../data/public/search/search_service.ts | 60 +- .../data/public/search/tabify/buckets.test.ts | 2 - .../public/search/tabify/get_columns.test.ts | 22 +- .../search/tabify/response_writer.test.ts | 20 +- .../data/public/search/tabify/tabify.test.ts | 16 +- .../brush_event.test.mocks.ts => services.ts} | 13 +- .../components/sidebar/state/reducers.ts | 18 +- .../public/legacy_imports.ts | 2 +- .../public/table_vis_controller.test.ts | 4 +- .../visualizations/public/legacy_imports.ts | 2 +- .../public/np_ready/public/vis_impl.js | 6 +- src/legacy/ui/public/agg_types/index.ts | 9 +- .../ui/public/vis/__tests__/_agg_config.js | 485 ------ .../ui/public/vis/__tests__/_agg_configs.js | 420 ----- .../data/common/field_formats/mocks.ts | 49 + src/plugins/data/public/mocks.ts | 28 +- .../data/public/search/search_source/mocks.ts | 19 - .../editor_frame_service/service.test.tsx | 4 - .../__snapshots__/zeek_details.test.tsx.snap | 1448 ++++++++--------- 102 files changed, 2646 insertions(+), 2101 deletions(-) create mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts rename src/legacy/{ui/public/vis/__tests__/index.js => core_plugins/data/public/search/aggs/test_helpers/index.ts} (86%) create mode 100644 src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts create mode 100644 src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts create mode 100644 src/legacy/core_plugins/data/public/search/mocks.ts rename src/legacy/core_plugins/data/public/{actions/filters/brush_event.test.mocks.ts => services.ts} (76%) delete mode 100644 src/legacy/ui/public/vis/__tests__/_agg_config.js delete mode 100644 src/legacy/ui/public/vis/__tests__/_agg_configs.js create mode 100644 src/plugins/data/common/field_formats/mocks.ts diff --git a/packages/kbn-utility-types/README.md b/packages/kbn-utility-types/README.md index 829fd21e143669..b57e98e379707e 100644 --- a/packages/kbn-utility-types/README.md +++ b/packages/kbn-utility-types/README.md @@ -18,6 +18,7 @@ type B = UnwrapPromise; // string ## Reference +- `Assign` — From `U` assign properties to `T` (just like object assign). - `Ensure` — Makes sure `T` is of type `X`. - `ObservableLike` — Minimal interface for an object resembling an `Observable`. - `PublicContract` — Returns an object with public keys only. diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 808935ed4cb5b8..657d9f547de66e 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -18,7 +18,7 @@ */ import { PromiseType } from 'utility-types'; -export { $Values, Required, Optional, Class } from 'utility-types'; +export { $Values, Assign, Class, Optional, Required } from 'utility-types'; /** * A type that may or may not be a `Promise`. diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts index 0e18c7c707fa3f..eb29530f92fee7 100644 --- a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts @@ -19,34 +19,14 @@ import moment from 'moment'; -jest.mock('../../search/aggs', () => ({ - AggConfigs: function AggConfigs() { - return { - createAggConfig: ({ params }: Record) => ({ - params, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }), - }; - }, -})); - -jest.mock('../../../../../../plugins/data/public/services', () => ({ - getIndexPatterns: () => { - return { - get: async () => { - return { - id: 'logstash-*', - timeFieldName: 'time', - }; - }, - }; - }, -})); - import { onBrushEvent, BrushEvent } from './brush_event'; +import { mockDataServices } from '../../search/aggs/test_helpers'; +import { IndexPatternsContract } from '../../../../../../plugins/data/public'; +import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setIndexPatterns } from '../../../../../../plugins/data/public/services'; + describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; @@ -59,11 +39,28 @@ describe('brushEvent', () => { }, getIndexPattern: () => ({ timeFieldName: 'time', + fields: { + getByName: () => undefined, + filter: () => [], + }, }), }, ]; beforeEach(() => { + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => ({ + id: 'indexPatternId', + timeFieldName: 'time', + fields: { + getByName: () => undefined, + filter: () => [], + }, + }), + } as unknown) as IndexPatternsContract); + baseEvent = { data: { ordered: { diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 8cde5d0a1fc115..8d730d18a17559 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -35,18 +35,18 @@ export { } from '../../../../plugins/data/public'; export { // agg_types - AggParam, - AggParamOption, - DateRangeKey, + AggParam, // only the type is used externally, only in vis editor + AggParamOption, // only the type is used externally + DateRangeKey, // only used in field formatter deserialization, which will live in data IAggConfig, IAggConfigs, IAggType, IFieldParamType, IMetricAggType, - IpRangeKey, + IpRangeKey, // only used in field formatter deserialization, which will live in data ISchemas, - OptionedParamEditorProps, - OptionedValueProp, + OptionedParamEditorProps, // only type is used externally + OptionedValueProp, // only type is used externally } from './search/types'; /** @public static code */ diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index e13e8e34eaebec..e2b8ca5dda78cf 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -36,6 +36,7 @@ import { setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +import { setSearchServiceShim } from './services'; import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; import { @@ -112,6 +113,9 @@ export class DataPlugin } public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { + const search = this.search.start(core); + setSearchServiceShim(search); + setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); @@ -123,7 +127,7 @@ export class DataPlugin uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return { - search: this.search.start(core), + search, }; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts new file mode 100644 index 00000000000000..7769aa29184d3a --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts @@ -0,0 +1,497 @@ +/* + * 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 { identity } from 'lodash'; + +import { AggConfig, IAggConfig } from './agg_config'; +import { AggConfigs, CreateAggConfigParams } from './agg_configs'; +import { AggType } from './agg_types'; +import { AggTypesRegistryStart } from './agg_types_registry'; +import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { stubIndexPatternWithFields } from '../../../../../../plugins/data/public/stubs'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setFieldFormats } from '../../../../../../plugins/data/public/services'; + +describe('AggConfig', () => { + let indexPattern: IndexPattern; + let typesRegistry: AggTypesRegistryStart; + + beforeEach(() => { + jest.restoreAllMocks(); + mockDataServices(); + indexPattern = stubIndexPatternWithFields as IndexPattern; + typesRegistry = mockAggTypesRegistry(); + }); + + describe('#toDsl', () => { + it('calls #write()', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const configStates = { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + + const spy = jest.spyOn(aggConfig, 'write').mockImplementation(() => ({ params: {} })); + aggConfig.toDsl(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('uses the type name as the agg name', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const configStates = { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + + jest.spyOn(aggConfig, 'write').mockImplementation(() => ({ params: {} })); + const dsl = aggConfig.toDsl(); + expect(dsl).toHaveProperty('date_histogram'); + }); + + it('uses the params from #write() output as the agg params', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const configStates = { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + + const football = {}; + jest.spyOn(aggConfig, 'write').mockImplementation(() => ({ params: football })); + const dsl = aggConfig.toDsl(); + expect(dsl.date_histogram).toBe(football); + }); + + it('includes subAggs from #write() output', () => { + const configStates = [ + { + enabled: true, + type: 'avg', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ]; + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + + const histoConfig = ac.byName('date_histogram')[0]; + const avgConfig = ac.byName('avg')[0]; + const football = {}; + + jest + .spyOn(histoConfig, 'write') + .mockImplementation(() => ({ params: {}, subAggs: [avgConfig] })); + jest.spyOn(avgConfig, 'write').mockImplementation(() => ({ params: football })); + + const dsl = histoConfig.toDsl(); + expect(dsl).toHaveProperty('aggs'); + expect(dsl.aggs).toHaveProperty(avgConfig.id); + expect(dsl.aggs[avgConfig.id]).toHaveProperty('avg'); + expect(dsl.aggs[avgConfig.id].avg).toBe(football); + }); + }); + + describe('::ensureIds', () => { + it('accepts an array of objects and assigns ids to them', () => { + const objs = [{}, {}, {}, {}]; + AggConfig.ensureIds(objs); + expect(objs[0]).toHaveProperty('id', '1'); + expect(objs[1]).toHaveProperty('id', '2'); + expect(objs[2]).toHaveProperty('id', '3'); + expect(objs[3]).toHaveProperty('id', '4'); + }); + + it('assigns ids relative to the other only item in the list', () => { + const objs = [{ id: '100' }, {}]; + AggConfig.ensureIds(objs); + expect(objs[0]).toHaveProperty('id', '100'); + expect(objs[1]).toHaveProperty('id', '101'); + }); + + it('assigns ids relative to the other items in the list', () => { + const objs = [{ id: '100' }, { id: '200' }, { id: '500' }, { id: '350' }, {}]; + AggConfig.ensureIds(objs); + expect(objs[0]).toHaveProperty('id', '100'); + expect(objs[1]).toHaveProperty('id', '200'); + expect(objs[2]).toHaveProperty('id', '500'); + expect(objs[3]).toHaveProperty('id', '350'); + expect(objs[4]).toHaveProperty('id', '501'); + }); + + it('uses ::nextId to get the starting value', () => { + jest.spyOn(AggConfig, 'nextId').mockImplementation(() => 534); + const objs = AggConfig.ensureIds([{}]); + expect(objs[0]).toHaveProperty('id', '534'); + }); + + it('only calls ::nextId once', () => { + const start = 420; + const spy = jest.spyOn(AggConfig, 'nextId').mockImplementation(() => start); + const objs = AggConfig.ensureIds([{}, {}, {}, {}, {}, {}, {}]); + + expect(spy).toHaveBeenCalledTimes(1); + objs.forEach((obj, i) => { + expect(obj).toHaveProperty('id', String(start + i)); + }); + }); + }); + + describe('::nextId', () => { + it('accepts a list of objects and picks the next id', () => { + const next = AggConfig.nextId([{ id: '100' }, { id: '500' }] as IAggConfig[]); + expect(next).toBe(501); + }); + + it('handles an empty list', () => { + const next = AggConfig.nextId([]); + expect(next).toBe(1); + }); + + it('fails when the list is not defined', () => { + expect(() => { + AggConfig.nextId((undefined as unknown) as IAggConfig[]); + }).toThrowError(); + }); + }); + + describe('#toJsonDataEquals', () => { + const testsIdentical = [ + [ + { + enabled: true, + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + ], + [ + { + enabled: true, + type: 'avg', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ], + ]; + + testsIdentical.forEach((configState, index) => { + it(`identical aggregations (${index})`, () => { + const ac1 = new AggConfigs(indexPattern, configState, { typesRegistry }); + const ac2 = new AggConfigs(indexPattern, configState, { typesRegistry }); + expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); + }); + }); + + const testsIdenticalDifferentOrder = [ + { + config1: [ + { + enabled: true, + type: 'avg', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ], + config2: [ + { + enabled: true, + schema: 'metric', + type: 'avg', + params: {}, + }, + { + enabled: true, + schema: 'segment', + type: 'date_histogram', + params: {}, + }, + ], + }, + ]; + + testsIdenticalDifferentOrder.forEach((test, index) => { + it(`identical aggregations (${index}) - init json is in different order`, () => { + const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); + const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); + expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); + }); + }); + + const testsDifferent = [ + { + config1: [ + { + enabled: true, + type: 'avg', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ], + config2: [ + { + enabled: true, + type: 'max', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ], + }, + { + config1: [ + { + enabled: true, + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + ], + config2: [ + { + enabled: true, + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ], + }, + ]; + + testsDifferent.forEach((test, index) => { + it(`different aggregations (${index})`, () => { + const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); + const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); + expect(ac1.jsonDataEquals(ac2.aggs)).toBe(false); + }); + }); + }); + + describe('#toJSON', () => { + it('includes the aggs id, params, type and schema', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const configStates = { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + + expect(aggConfig.id).toBe('1'); + expect(typeof aggConfig.params).toBe('object'); + expect(aggConfig.type).toBeInstanceOf(AggType); + expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); + expect(typeof aggConfig.schema).toBe('object'); + expect(aggConfig.schema).toHaveProperty('name', 'segment'); + + const state = aggConfig.toJSON(); + expect(state).toHaveProperty('id', '1'); + expect(typeof state.params).toBe('object'); + expect(state).toHaveProperty('type', 'date_histogram'); + expect(state).toHaveProperty('schema', 'segment'); + }); + + it('test serialization order is identical (for visual consistency)', () => { + const configStates = [ + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ]; + const ac1 = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac2 = new AggConfigs(indexPattern, configStates, { typesRegistry }); + + // this relies on the assumption that js-engines consistently loop over properties in insertion order. + // most likely the case, but strictly speaking not guaranteed by the JS and JSON specifications. + expect(JSON.stringify(ac1.aggs) === JSON.stringify(ac2.aggs)).toBe(true); + }); + }); + + describe('#makeLabel', () => { + let aggConfig: AggConfig; + + beforeEach(() => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + aggConfig = ac.createAggConfig({ type: 'count' } as CreateAggConfigParams); + }); + + it('uses the custom label if it is defined', () => { + aggConfig.params.customLabel = 'Custom label'; + const label = aggConfig.makeLabel(); + expect(label).toBe(aggConfig.params.customLabel); + }); + + it('default label should be "Count"', () => { + const label = aggConfig.makeLabel(); + expect(label).toBe('Count'); + }); + + it('default label should be "Percentage of Count" when percentageMode is set to true', () => { + const label = aggConfig.makeLabel(true); + expect(label).toBe('Percentage of Count'); + }); + + it('empty label if the type is not defined', () => { + aggConfig.type = (undefined as unknown) as AggType; + const label = aggConfig.makeLabel(); + expect(label).toBe(''); + }); + }); + + describe('#fieldFormatter - custom getFormat handler', () => { + it('returns formatter from getFormat handler', () => { + setFieldFormats({ + ...dataPluginMock.createStartContract().fieldFormats, + getDefaultInstance: jest.fn().mockImplementation(() => ({ + getConverterFor: jest.fn().mockImplementation(() => (t: string) => t), + })) as any, + }); + + const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const configStates = { + enabled: true, + type: 'count', + schema: 'metric', + params: { field: '@timestamp' }, + }; + const aggConfig = ac.createAggConfig(configStates); + + const fieldFormatter = aggConfig.fieldFormatter(); + expect(fieldFormatter).toBeDefined(); + expect(fieldFormatter('text')).toBe('text'); + }); + }); + + // TODO: Converting these field formatter tests from browser tests to unit + // tests makes them much less helpful due to the extensive use of mocking. + // We should revisit these and rewrite them into something more useful. + describe('#fieldFormatter - no custom getFormat handler', () => { + let aggConfig: AggConfig; + + beforeEach(() => { + setFieldFormats({ + ...dataPluginMock.createStartContract().fieldFormats, + getDefaultInstance: jest.fn().mockImplementation(() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any, + }); + indexPattern.fields.getByName = name => + ({ + format: { + getConverterFor: (t?: string) => t || identity, + }, + } as IndexPatternField); + + const configStates = { + enabled: true, + type: 'histogram', + schema: 'bucket', + params: { + field: { + format: { + getConverterFor: (t?: string) => t || identity, + }, + }, + }, + }; + const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry }); + aggConfig = ac.createAggConfig(configStates); + }); + + it("returns the field's formatter", () => { + expect(aggConfig.fieldFormatter().toString()).toBe( + aggConfig + .getField() + .format.getConverterFor() + .toString() + ); + }); + + it('returns the string format if the field does not have a format', () => { + const agg = aggConfig; + agg.params.field = { type: 'number', format: null }; + const fieldFormatter = agg.fieldFormatter(); + expect(fieldFormatter).toBeDefined(); + expect(fieldFormatter('text')).toBe('text'); + }); + + it('returns the string format if there is no field', () => { + const agg = aggConfig; + delete agg.params.field; + const fieldFormatter = agg.fieldFormatter(); + expect(fieldFormatter).toBeDefined(); + expect(fieldFormatter('text')).toBe('text'); + }); + + it('returns the html converter if "html" is passed in', () => { + const field = indexPattern.fields.getByName('bytes'); + expect(aggConfig.fieldFormatter('html').toString()).toBe( + field!.format.getConverterFor('html').toString() + ); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts index 2b21c5c4868a52..659bec3f702e37 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts @@ -17,16 +17,8 @@ * under the License. */ -/** - * @name AggConfig - * - * @description This class represents an aggregation, which is displayed in the left-hand nav of - * the Visualize app. - */ - import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { IAggType } from './agg_type'; import { AggGroupNames } from './agg_groups'; import { writeParams } from './agg_params'; @@ -38,18 +30,20 @@ import { FieldFormatsContentType, KBN_FIELD_TYPES, } from '../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../plugins/data/public/services'; export interface AggConfigOptions { - enabled: boolean; - type: string; - params: any; + type: IAggType; + enabled?: boolean; id?: string; - schema?: string; + params?: Record; + schema?: string | Schema; } const unknownSchema: Schema = { name: 'unknown', - title: 'Unknown', + title: 'Unknown', // only here for illustrative purposes hideCustomLabel: true, aggFilter: [], min: 1, @@ -65,21 +59,6 @@ const unknownSchema: Schema = { }, }; -const getTypeFromRegistry = (type: string): IAggType => { - // We need to inline require here, since we're having a cyclic dependency - // from somewhere inside agg_types back to AggConfig. - const aggTypes = require('../aggs').aggTypes; - const registeredType = - aggTypes.metrics.find((agg: IAggType) => agg.name === type) || - aggTypes.buckets.find((agg: IAggType) => agg.name === type); - - if (!registeredType) { - throw new Error('unknown type'); - } - - return registeredType; -}; - const getSchemaFromRegistry = (schemas: any, schema: string): Schema => { let registeredSchema = schemas ? schemas.byName[schema] : null; if (!registeredSchema) { @@ -90,6 +69,13 @@ const getSchemaFromRegistry = (schemas: any, schema: string): Schema => { return registeredSchema; }; +/** + * @name AggConfig + * + * @description This class represents an aggregation, which is displayed in the left-hand nav of + * the Visualize app. + */ + // TODO need to make a more explicit interface for this export type IAggConfig = AggConfig; @@ -101,9 +87,9 @@ export class AggConfig { * @param {array[object]} list - a list of objects, objects can be anything really * @return {array} - the list that was passed in */ - static ensureIds(list: AggConfig[]) { - const have: AggConfig[] = []; - const haveNot: AggConfig[] = []; + static ensureIds(list: any[]) { + const have: IAggConfig[] = []; + const haveNot: AggConfigOptions[] = []; list.forEach(function(obj) { (obj.id ? have : haveNot).push(obj); }); @@ -121,7 +107,7 @@ export class AggConfig { * * @return {array} list - a list of objects with id properties */ - static nextId(list: AggConfig[]) { + static nextId(list: IAggConfig[]) { return ( 1 + list.reduce(function(max, obj) { @@ -161,10 +147,10 @@ export class AggConfig { // set the params to the values from opts, or just to the defaults this.setParams(opts.params || {}); - // @ts-ignore - this.__type = this.__type; // @ts-ignore this.__schema = this.__schema; + // @ts-ignore + this.__type = this.__type; } /** @@ -394,7 +380,8 @@ export class AggConfig { } fieldOwnFormatter(contentType?: FieldFormatsContentType, defaultFormat?: any) { - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); + const field = this.getField(); let format = field && field.format; if (!format) format = defaultFormat; @@ -456,8 +443,8 @@ export class AggConfig { }); } - public setType(type: string | IAggType) { - this.type = typeof type === 'string' ? getTypeFromRegistry(type) : type; + public setType(type: IAggType) { + this.type = type; } public get schema() { diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts new file mode 100644 index 00000000000000..29f16b1e4f0bf2 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts @@ -0,0 +1,503 @@ +/* + * 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 { indexBy } from 'lodash'; +import { AggConfig } from './agg_config'; +import { AggConfigs } from './agg_configs'; +import { AggTypesRegistryStart } from './agg_types_registry'; +import { Schemas } from './schemas'; +import { AggGroupNames } from './agg_groups'; +import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; +import { + stubIndexPattern, + stubIndexPatternWithFields, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/stubs'; + +describe('AggConfigs', () => { + let indexPattern: IndexPattern; + let typesRegistry: AggTypesRegistryStart; + + beforeEach(() => { + indexPattern = stubIndexPatternWithFields as IndexPattern; + typesRegistry = mockAggTypesRegistry(); + }); + + describe('constructor', () => { + it('handles passing just a type', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(ac.aggs).toHaveLength(1); + }); + + it('attempts to ensure that all states have an id', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + params: {}, + }, + { + enabled: true, + type: 'terms', + params: {}, + schema: 'split', + }, + ]; + + const spy = jest.spyOn(AggConfig, 'ensureIds'); + new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0]).toEqual([configStates]); + spy.mockRestore(); + }); + + describe('defaults', () => { + const schemas = new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: 'Simple', + min: 1, + max: 2, + defaults: [ + { schema: 'metric', type: 'count' }, + { schema: 'metric', type: 'avg' }, + { schema: 'metric', type: 'sum' }, + ], + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: 'Example', + min: 0, + max: 1, + defaults: [ + { schema: 'segment', type: 'terms' }, + { schema: 'segment', type: 'filters' }, + ], + }, + ]); + + it('should only set the number of defaults defined by the max', () => { + const ac = new AggConfigs(indexPattern, [], { + schemas: schemas.all, + typesRegistry, + }); + expect(ac.bySchemaName('metric')).toHaveLength(2); + }); + + it('should set the defaults defined in the schema when none exist', () => { + const ac = new AggConfigs(indexPattern, [], { + schemas: schemas.all, + typesRegistry, + }); + expect(ac.aggs).toHaveLength(3); + }); + + it('should NOT set the defaults defined in the schema when some exist', () => { + const configStates = [ + { + enabled: true, + type: 'date_histogram', + params: {}, + schema: 'segment', + }, + ]; + const ac = new AggConfigs(indexPattern, configStates, { + schemas: schemas.all, + typesRegistry, + }); + expect(ac.aggs).toHaveLength(3); + expect(ac.bySchemaName('segment')[0].type.name).toEqual('date_histogram'); + }); + }); + }); + + describe('#createAggConfig', () => { + it('accepts a configState which is provided as an AggConfig object', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(ac.aggs).toHaveLength(2); + + ac.createAggConfig( + new AggConfig(ac, { + enabled: true, + type: typesRegistry.get('terms'), + params: {}, + schema: 'split', + }) + ); + expect(ac.aggs).toHaveLength(3); + }); + + it('adds new AggConfig entries to AggConfigs by default', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(ac.aggs).toHaveLength(1); + + ac.createAggConfig({ + enabled: true, + type: 'terms', + params: {}, + schema: 'split', + }); + expect(ac.aggs).toHaveLength(2); + }); + + it('does not add an agg to AggConfigs if addToAggConfigs: false', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(ac.aggs).toHaveLength(1); + + ac.createAggConfig( + { + enabled: true, + type: 'terms', + params: {}, + schema: 'split', + }, + { addToAggConfigs: false } + ); + expect(ac.aggs).toHaveLength(1); + }); + }); + + describe('#getRequestAggs', () => { + it('performs a stable sort, but moves metrics to the bottom', () => { + const configStates = [ + { type: 'avg', enabled: true, params: {}, schema: 'metric' }, + { type: 'terms', enabled: true, params: {}, schema: 'split' }, + { type: 'histogram', enabled: true, params: {}, schema: 'split' }, + { type: 'sum', enabled: true, params: {}, schema: 'metric' }, + { type: 'date_histogram', enabled: true, params: {}, schema: 'segment' }, + { type: 'filters', enabled: true, params: {}, schema: 'split' }, + { type: 'percentiles', enabled: true, params: {}, schema: 'metric' }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const sorted = ac.getRequestAggs(); + const aggs = indexBy(ac.aggs, agg => agg.type.name); + + expect(sorted.shift()).toBe(aggs.terms); + expect(sorted.shift()).toBe(aggs.histogram); + expect(sorted.shift()).toBe(aggs.date_histogram); + expect(sorted.shift()).toBe(aggs.filters); + expect(sorted.shift()).toBe(aggs.avg); + expect(sorted.shift()).toBe(aggs.sum); + expect(sorted.shift()).toBe(aggs.percentiles); + expect(sorted).toHaveLength(0); + }); + }); + + describe('#getResponseAggs', () => { + it('returns all request aggs for basic aggs', () => { + const configStates = [ + { type: 'terms', enabled: true, params: {}, schema: 'split' }, + { type: 'date_histogram', enabled: true, params: {}, schema: 'segment' }, + { type: 'count', enabled: true, params: {}, schema: 'metric' }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const sorted = ac.getResponseAggs(); + const aggs = indexBy(ac.aggs, agg => agg.type.name); + + expect(sorted.shift()).toBe(aggs.terms); + expect(sorted.shift()).toBe(aggs.date_histogram); + expect(sorted.shift()).toBe(aggs.count); + expect(sorted).toHaveLength(0); + }); + + it('expands aggs that have multiple responses', () => { + const configStates = [ + { type: 'terms', enabled: true, params: {}, schema: 'split' }, + { type: 'date_histogram', enabled: true, params: {}, schema: 'segment' }, + { type: 'percentiles', enabled: true, params: { percents: [1, 2, 3] }, schema: 'metric' }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const sorted = ac.getResponseAggs(); + const aggs = indexBy(ac.aggs, agg => agg.type.name); + + expect(sorted.shift()).toBe(aggs.terms); + expect(sorted.shift()).toBe(aggs.date_histogram); + expect(sorted.shift()!.id!).toBe(aggs.percentiles.id + '.' + 1); + expect(sorted.shift()!.id!).toBe(aggs.percentiles.id + '.' + 2); + expect(sorted.shift()!.id!).toBe(aggs.percentiles.id + '.' + 3); + expect(sorted).toHaveLength(0); + }); + }); + + describe('#toDsl', () => { + const schemas = new Schemas([ + { + group: AggGroupNames.Buckets, + name: 'segment', + }, + { + group: AggGroupNames.Buckets, + name: 'split', + }, + ]); + + beforeEach(() => { + mockDataServices(); + indexPattern = stubIndexPattern as IndexPattern; + indexPattern.fields.getByName = name => (name as unknown) as IndexPatternField; + }); + + it('uses the sorted aggs', () => { + const configStates = [{ enabled: true, type: 'avg', params: { field: 'bytes' } }]; + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const spy = jest.spyOn(AggConfigs.prototype, 'getRequestAggs'); + ac.toDsl(); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); + + it('calls aggConfig#toDsl() on each aggConfig and compiles the nested output', () => { + const configStates = [ + { enabled: true, type: 'date_histogram', params: {}, schema: 'segment' }, + { enabled: true, type: 'terms', params: {}, schema: 'split' }, + { enabled: true, type: 'count', params: {} }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { + typesRegistry, + schemas: schemas.all, + }); + + const aggInfos = ac.aggs.map(aggConfig => { + const football = {}; + aggConfig.toDsl = jest.fn().mockImplementation(() => football); + + return { + id: aggConfig.id, + football, + }; + }); + + (function recurse(lvl: Record): void { + const info = aggInfos.shift(); + if (!info) return; + + expect(lvl).toHaveProperty(info.id); + expect(lvl[info.id]).toBe(info.football); + + if (lvl[info.id].aggs) { + return recurse(lvl[info.id].aggs); + } + })(ac.toDsl()); + + expect(aggInfos).toHaveLength(1); + }); + + it("skips aggs that don't have a dsl representation", () => { + const configStates = [ + { + enabled: true, + type: 'date_histogram', + params: { field: '@timestamp', interval: '10s' }, + schema: 'segment', + }, + { + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const dsl = ac.toDsl(); + const histo = ac.byName('date_histogram')[0]; + const count = ac.byName('count')[0]; + + expect(dsl).toHaveProperty(histo.id); + expect(typeof dsl[histo.id]).toBe('object'); + expect(dsl[histo.id]).not.toHaveProperty('aggs'); + expect(dsl).not.toHaveProperty(count.id); + }); + + it('writes multiple metric aggregations at the same level', () => { + const configStates = [ + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'min', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { + typesRegistry, + schemas: schemas.all, + }); + const dsl = ac.toDsl(); + const histo = ac.byName('date_histogram')[0]; + const metrics = ac.bySchemaGroup('metrics'); + + expect(dsl).toHaveProperty(histo.id); + expect(typeof dsl[histo.id]).toBe('object'); + expect(dsl[histo.id]).toHaveProperty('aggs'); + + metrics.forEach(metric => { + expect(dsl[histo.id].aggs).toHaveProperty(metric.id); + expect(dsl[histo.id].aggs[metric.id]).not.toHaveProperty('aggs'); + }); + }); + + it('writes multiple metric aggregations at every level if the vis is hierarchical', () => { + const configStates = [ + { enabled: true, type: 'terms', schema: 'segment', params: { field: 'bytes', orderBy: 1 } }, + { enabled: true, type: 'terms', schema: 'segment', params: { field: 'bytes', orderBy: 1 } }, + { enabled: true, id: '1', type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'min', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const topLevelDsl = ac.toDsl(true); + const buckets = ac.bySchemaGroup('buckets'); + const metrics = ac.bySchemaGroup('metrics'); + + (function checkLevel(dsl) { + const bucket = buckets.shift(); + if (!bucket) return; + + expect(dsl).toHaveProperty(bucket.id); + + expect(typeof dsl[bucket.id]).toBe('object'); + expect(dsl[bucket.id]).toHaveProperty('aggs'); + + metrics.forEach((metric: AggConfig) => { + expect(dsl[bucket.id].aggs).toHaveProperty(metric.id); + expect(dsl[bucket.id].aggs[metric.id]).not.toHaveProperty('aggs'); + }); + + if (buckets.length) { + checkLevel(dsl[bucket.id].aggs); + } + })(topLevelDsl); + }); + + it('adds the parent aggs of nested metrics at every level if the vis is hierarchical', () => { + const configStates = [ + { + enabled: true, + id: '1', + type: 'avg_bucket', + schema: 'metric', + params: { + customBucket: { + id: '1-bucket', + type: 'date_histogram', + schema: 'bucketAgg', + params: { + field: '@timestamp', + interval: '10s', + }, + }, + customMetric: { + id: '1-metric', + type: 'count', + schema: 'metricAgg', + params: {}, + }, + }, + }, + { + enabled: true, + id: '2', + type: 'terms', + schema: 'bucket', + params: { + field: 'clientip', + }, + }, + { + enabled: true, + id: '3', + type: 'terms', + schema: 'bucket', + params: { + field: 'machine.os.raw', + }, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const topLevelDsl = ac.toDsl(true)['2']; + + expect(Object.keys(topLevelDsl.aggs)).toContain('1'); + expect(Object.keys(topLevelDsl.aggs)).toContain('1-bucket'); + expect(topLevelDsl.aggs['1'].avg_bucket).toHaveProperty('buckets_path', '1-bucket>_count'); + expect(Object.keys(topLevelDsl.aggs['3'].aggs)).toContain('1'); + expect(Object.keys(topLevelDsl.aggs['3'].aggs)).toContain('1-bucket'); + expect(topLevelDsl.aggs['3'].aggs['1'].avg_bucket).toHaveProperty( + 'buckets_path', + '1-bucket>_count' + ); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index 8e091ed5f21ae0..ab70e66b1e138c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -17,17 +17,12 @@ * under the License. */ -/** - * @name AggConfig - * - * @extends IndexedArray - * - * @description A "data structure"-like class with methods for indexing and - * accessing instances of AggConfig. - */ - import _ from 'lodash'; +import { Assign } from '@kbn/utility-types'; + import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; +import { IAggType } from './agg_type'; +import { AggTypesRegistryStart } from './agg_types_registry'; import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { @@ -55,6 +50,24 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) { } } +export interface AggConfigsOptions { + schemas?: Schemas; + typesRegistry: AggTypesRegistryStart; +} + +export type CreateAggConfigParams = Assign; + +/** + * @name AggConfigs + * + * @description A "data structure"-like class with methods for indexing and + * accessing instances of AggConfig. This should never be instantiated directly + * outside of this plugin. Rather, downstream plugins should do this via + * `createAggConfigs()` + * + * @internal + */ + // TODO need to make a more explicit interface for this export type IAggConfigs = AggConfigs; @@ -62,23 +75,31 @@ export class AggConfigs { public indexPattern: IndexPattern; public schemas: any; public timeRange?: TimeRange; + private readonly typesRegistry: AggTypesRegistryStart; aggs: IAggConfig[]; - constructor(indexPattern: IndexPattern, configStates = [] as any, schemas?: any) { + constructor( + indexPattern: IndexPattern, + configStates: CreateAggConfigParams[] = [], + opts: AggConfigsOptions + ) { + this.typesRegistry = opts.typesRegistry; + configStates = AggConfig.ensureIds(configStates); this.aggs = []; this.indexPattern = indexPattern; - this.schemas = schemas; + this.schemas = opts.schemas; configStates.forEach((params: any) => this.createAggConfig(params)); - if (schemas) { - this.initializeDefaultsFromSchemas(schemas); + if (this.schemas) { + this.initializeDefaultsFromSchemas(this.schemas); } } + // do this wherever the schemas were passed in, & pass in state defaults instead initializeDefaultsFromSchemas(schemas: Schemas) { // Set the defaults for any schema which has them. If the defaults // for some reason has more then the max only set the max number @@ -91,10 +112,11 @@ export class AggConfigs { }) .each((schema: any) => { if (!this.aggs.find((agg: AggConfig) => agg.schema && agg.schema.name === schema.name)) { + // the result here should be passable as a configState const defaults = schema.defaults.slice(0, schema.max); _.each(defaults, defaultState => { const state = _.defaults({ id: AggConfig.nextId(this.aggs) }, defaultState); - this.aggs.push(new AggConfig(this, state as AggConfigOptions)); + this.createAggConfig(state as AggConfigOptions); }); } }) @@ -124,28 +146,36 @@ export class AggConfigs { if (!enabledOnly) return true; return agg.enabled; }; - const aggConfigs = new AggConfigs( - this.indexPattern, - this.aggs.filter(filterAggs), - this.schemas - ); + + const aggConfigs = new AggConfigs(this.indexPattern, this.aggs.filter(filterAggs), { + schemas: this.schemas, + typesRegistry: this.typesRegistry, + }); + return aggConfigs; } createAggConfig = ( - params: AggConfig | AggConfigOptions, + params: CreateAggConfigParams, { addToAggConfigs = true } = {} ) => { + const { type } = params; let aggConfig; + if (params instanceof AggConfig) { aggConfig = params; params.parent = this; } else { - aggConfig = new AggConfig(this, params); + aggConfig = new AggConfig(this, { + ...params, + type: typeof type === 'string' ? this.typesRegistry.get(type) : type, + }); } + if (addToAggConfigs) { this.aggs.push(aggConfig); } + return aggConfig as T; }; @@ -166,10 +196,10 @@ export class AggConfigs { return true; } - toDsl(hierarchical: boolean = false) { + toDsl(hierarchical: boolean = false): Record { const dslTopLvl = {}; let dslLvlCursor: Record; - let nestedMetrics: Array<{ config: AggConfig; dsl: any }> | []; + let nestedMetrics: Array<{ config: AggConfig; dsl: Record }> | []; if (hierarchical) { // collect all metrics, and filter out the ones that we won't be copying diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts index 30ab272537dad1..b08fcf309e9ed6 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_params.test.ts @@ -23,8 +23,6 @@ import { FieldParamType } from './param_types/field'; import { OptionedParamType } from './param_types/optioned'; import { AggParamType } from '../aggs/param_types/agg'; -jest.mock('ui/new_platform'); - describe('AggParams class', () => { describe('constructor args', () => { it('accepts an array of param defs', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts index 6d4c2d1317f505..c78e56dd25887d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_type.test.ts @@ -19,11 +19,16 @@ import { AggType, AggTypeConfig } from './agg_type'; import { IAggConfig } from './agg_config'; -import { npStart } from 'ui/new_platform'; - -jest.mock('ui/new_platform'); +import { mockDataServices } from './test_helpers'; +import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setFieldFormats } from '../../../../../../plugins/data/public/services'; describe('AggType Class', () => { + beforeEach(() => { + mockDataServices(); + }); + describe('constructor', () => { it("requires a valid config object as it's first param", () => { expect(() => { @@ -153,7 +158,10 @@ describe('AggType Class', () => { }); it('returns default formatter', () => { - npStart.plugins.data.fieldFormats.getDefaultInstance = jest.fn(() => 'default') as any; + setFieldFormats({ + ...dataPluginMock.createStartContract().fieldFormats, + getDefaultInstance: jest.fn(() => 'default') as any, + }); const aggType = new AggType({ name: 'name', diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts index 5ccf0f65c0e921..3cd9496d3f23d0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_type.ts @@ -19,7 +19,6 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { initParams } from './agg_params'; import { AggConfig } from './agg_config'; @@ -32,6 +31,8 @@ import { IFieldFormat, ISearchSource, } from '../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../plugins/data/public/services'; export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, @@ -65,7 +66,7 @@ export interface AggTypeConfig< const getFormat = (agg: AggConfig) => { const field = agg.getField(); - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); return field ? field.format : fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.STRING); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts new file mode 100644 index 00000000000000..405f83e237de83 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { + AggTypesRegistry, + AggTypesRegistrySetup, + AggTypesRegistryStart, +} from './agg_types_registry'; +import { BucketAggType } from './buckets/_bucket_agg_type'; +import { MetricAggType } from './metrics/metric_agg_type'; + +const bucketType = { name: 'terms', type: 'bucket' } as BucketAggType; +const metricType = { name: 'count', type: 'metric' } as MetricAggType; + +describe('AggTypesRegistry', () => { + let registry: AggTypesRegistry; + let setup: AggTypesRegistrySetup; + let start: AggTypesRegistryStart; + + beforeEach(() => { + registry = new AggTypesRegistry(); + setup = registry.setup(); + start = registry.start(); + }); + + it('registerBucket adds new buckets', () => { + setup.registerBucket(bucketType); + expect(start.getBuckets()).toEqual([bucketType]); + }); + + it('registerBucket throws error when registering duplicate bucket', () => { + expect(() => { + setup.registerBucket(bucketType); + setup.registerBucket(bucketType); + }).toThrow(/already been registered with name: terms/); + }); + + it('registerMetric adds new metrics', () => { + setup.registerMetric(metricType); + expect(start.getMetrics()).toEqual([metricType]); + }); + + it('registerMetric throws error when registering duplicate metric', () => { + expect(() => { + setup.registerMetric(metricType); + setup.registerMetric(metricType); + }).toThrow(/already been registered with name: count/); + }); + + it('gets either buckets or metrics by id', () => { + setup.registerBucket(bucketType); + setup.registerMetric(metricType); + expect(start.get('terms')).toEqual(bucketType); + expect(start.get('count')).toEqual(metricType); + }); + + it('getBuckets retrieves only buckets', () => { + setup.registerBucket(bucketType); + expect(start.getBuckets()).toEqual([bucketType]); + }); + + it('getMetrics retrieves only metrics', () => { + setup.registerMetric(metricType); + expect(start.getMetrics()).toEqual([metricType]); + }); + + it('getAll returns all buckets and metrics', () => { + setup.registerBucket(bucketType); + setup.registerMetric(metricType); + expect(start.getAll()).toEqual({ + buckets: [bucketType], + metrics: [metricType], + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts new file mode 100644 index 00000000000000..8a8746106ae587 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_types_registry.ts @@ -0,0 +1,68 @@ +/* + * 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 { BucketAggType } from './buckets/_bucket_agg_type'; +import { MetricAggType } from './metrics/metric_agg_type'; + +export type AggTypesRegistrySetup = ReturnType; +export type AggTypesRegistryStart = ReturnType; + +export class AggTypesRegistry { + private readonly bucketAggs = new Map(); + private readonly metricAggs = new Map(); + + setup = () => { + return { + registerBucket: >(type: T): void => { + const { name } = type; + if (this.bucketAggs.get(name)) { + throw new Error(`Bucket agg has already been registered with name: ${name}`); + } + this.bucketAggs.set(name, type); + }, + registerMetric: >(type: T): void => { + const { name } = type; + if (this.metricAggs.get(name)) { + throw new Error(`Metric agg has already been registered with name: ${name}`); + } + this.metricAggs.set(name, type); + }, + }; + }; + + start = () => { + return { + get: (name: string) => { + return this.bucketAggs.get(name) || this.metricAggs.get(name); + }, + getBuckets: () => { + return Array.from(this.bucketAggs.values()); + }, + getMetrics: () => { + return Array.from(this.metricAggs.values()); + }, + getAll: () => { + return { + buckets: Array.from(this.bucketAggs.values()), + metrics: Array.from(this.metricAggs.values()), + }; + }, + }; + }; +} diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts index 546d054c5af978..d6ab58d5250a8c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_bucket_agg_type.ts @@ -17,16 +17,16 @@ * under the License. */ -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; -export interface IBucketAggConfig extends AggConfig { +export interface IBucketAggConfig extends IAggConfig { type: InstanceType; } -export interface BucketAggParam +export interface BucketAggParam extends AggParamType { scriptable?: boolean; filterFieldTypes?: KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; @@ -34,12 +34,12 @@ export interface BucketAggParam const bucketType = 'buckets'; -interface BucketAggTypeConfig +interface BucketAggTypeConfig extends AggTypeConfig> { - getKey?: (bucket: any, key: any, agg: AggConfig) => any; + getKey?: (bucket: any, key: any, agg: IAggConfig) => any; } -export class BucketAggType extends AggType< +export class BucketAggType extends AggType< TBucketAggConfig, BucketAggParam > { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts index e196687607d198..393d3b745250f4 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_interval_options.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { i18n } from '@kbn/i18n'; import { IBucketAggConfig } from './_bucket_agg_type'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 0d3f58c50a42e7..2b47dc384bca2f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -21,14 +21,22 @@ import moment from 'moment'; import { createFilterDateHistogram } from './date_histogram'; import { intervalOptions } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; -import { IBucketDateHistogramAggConfig } from '../date_histogram'; +import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; +import { dateHistogramBucketAgg, IBucketDateHistogramAggConfig } from '../date_histogram'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { RangeFilter } from '../../../../../../../../plugins/data/public'; +// TODO: remove this once time buckets is migrated jest.mock('ui/new_platform'); describe('AggConfig Filters', () => { describe('date_histogram', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry([dateHistogramBucketAgg]); + let agg: IBucketDateHistogramAggConfig; let filter: RangeFilter; let bucketStart: any; @@ -56,7 +64,7 @@ describe('AggConfig Filters', () => { params: { field: field.name, interval, customInterval: '5d' }, }, ], - null + { typesRegistry } ); const bucketKey = 1422579600000; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts index 41e806668337e2..c594c7718e58bb 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts @@ -18,16 +18,17 @@ */ import moment from 'moment'; +import { dateRangeBucketAgg } from '../date_range'; import { createFilterDateRange } from './date_range'; import { fieldFormats, FieldFormatsGetConfigFn } from '../../../../../../../../plugins/data/public'; import { AggConfigs } from '../../agg_configs'; +import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('Date range', () => { + const typesRegistry = mockAggTypesRegistry([dateRangeBucketAgg]); const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -55,7 +56,7 @@ describe('AggConfig Filters', () => { }, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts index 34cf996826865f..3b9c771e0f15f9 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts @@ -16,14 +16,21 @@ * specific language governing permissions and limitations * under the License. */ + +import { filtersBucketAgg } from '../filters'; import { createFilterFilters } from './filters'; import { AggConfigs } from '../../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; import { IBucketAggConfig } from '../_bucket_agg_type'; -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('filters', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry([filtersBucketAgg]); + const getAggConfigs = () => { const field = { name: 'bytes', @@ -52,7 +59,7 @@ describe('AggConfig Filters', () => { }, }, ], - null + { typesRegistry } ); }; it('should return a filters filter', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts index 9f845847df5d9d..b046c802c58c15 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts @@ -16,16 +16,22 @@ * specific language governing permissions and limitations * under the License. */ + import { createFilterHistogram } from './histogram'; import { AggConfigs } from '../../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { fieldFormats, FieldFormatsGetConfigFn } from '../../../../../../../../plugins/data/public'; -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('histogram', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry(); + const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -55,7 +61,7 @@ describe('AggConfig Filters', () => { }, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts index e92ba5cb2852a1..7572c48390dc25 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts @@ -17,17 +17,18 @@ * under the License. */ +import { ipRangeBucketAgg } from '../ip_range'; import { createFilterIpRange } from './ip_range'; -import { AggConfigs } from '../../agg_configs'; +import { AggConfigs, CreateAggConfigParams } from '../../agg_configs'; +import { mockAggTypesRegistry } from '../../test_helpers'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('IP range', () => { - const getAggConfigs = (aggs: Array>) => { + const typesRegistry = mockAggTypesRegistry([ipRangeBucketAgg]); + const getAggConfigs = (aggs: CreateAggConfigParams[]) => { const field = { name: 'ip', format: fieldFormats.IpFormat, @@ -42,7 +43,7 @@ describe('AggConfig Filters', () => { }, } as any; - return new AggConfigs(indexPattern, aggs, null); + return new AggConfigs(indexPattern, aggs, { typesRegistry }); }; it('should return a range filter for ip_range agg', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts index 33344ca0a34845..324d4252908324 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/range.test.ts @@ -17,16 +17,22 @@ * under the License. */ +import { rangeBucketAgg } from '../range'; import { createFilterRange } from './range'; import { fieldFormats, FieldFormatsGetConfigFn } from '../../../../../../../../plugins/data/public'; import { AggConfigs } from '../../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('range', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry([rangeBucketAgg]); + const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -56,7 +62,7 @@ describe('AggConfig Filters', () => { }, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index 7c6e769437ca1d..6db6eb11a5f527 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -17,17 +17,18 @@ * under the License. */ +import { termsBucketAgg } from '../terms'; import { createFilterTerms } from './terms'; -import { AggConfigs } from '../../agg_configs'; +import { AggConfigs, CreateAggConfigParams } from '../../agg_configs'; +import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { Filter, ExistsFilter } from '../../../../../../../../plugins/data/public'; -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('terms', () => { - const getAggConfigs = (aggs: Array>) => { + const typesRegistry = mockAggTypesRegistry([termsBucketAgg]); + const getAggConfigs = (aggs: CreateAggConfigParams[]) => { const indexPattern = { id: '1234', title: 'logstash-*', @@ -42,7 +43,7 @@ describe('AggConfig Filters', () => { indexPattern, }; - return new AggConfigs(indexPattern, aggs, null); + return new AggConfigs(indexPattern, aggs, { typesRegistry }); }; it('should return a match_phrase filter for terms', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts index dc0f9baa6d0cc7..a5368135728d49 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -21,8 +21,7 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import { timefilter } from 'ui/timefilter'; +// TODO need to move TimeBuckets import { TimeBuckets } from 'ui/time_buckets'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -33,6 +32,8 @@ import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getQueryService, getUiSettings } from '../../../../../../../plugins/data/public/services'; const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); @@ -40,6 +41,7 @@ const tzOffset = moment().format('Z'); const getInterval = (agg: IBucketAggConfig): string => _.get(agg, ['params', 'interval']); export const setBounds = (agg: IBucketDateHistogramAggConfig, force?: boolean) => { + const { timefilter } = getQueryService().timefilter; if (agg.buckets._alreadySet && !force) return; agg.buckets._alreadySet = true; const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; @@ -221,7 +223,7 @@ export const dateHistogramBucketAgg = new BucketAggType { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry([dateRangeBucketAgg]); + const getAggConfigs = (params: Record = {}, hasIncludeTypeMeta: boolean = true) => { const field = { name: 'bytes', @@ -58,7 +67,7 @@ describe('date_range params', () => { params, }, ], - null + { typesRegistry } ); }; @@ -95,7 +104,11 @@ describe('date_range params', () => { }); it('should use the Kibana time_zone if no parameter specified', () => { - npStart.core.uiSettings.get = jest.fn(() => 'kibanaTimeZone' as any); + const core = coreMock.createStart(); + setUiSettings({ + ...core.uiSettings, + get: () => 'kibanaTimeZone' as any, + }); const aggConfigs = getAggConfigs( { @@ -106,6 +119,8 @@ describe('date_range params', () => { const dateRange = aggConfigs.aggs[0]; const params = dateRange.toDsl()[BUCKET_TYPES.DATE_RANGE]; + setUiSettings(core.uiSettings); // clean up + expect(params.time_zone).toBe('kibanaTimeZone'); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts index 1dc24ca80035c0..933cdd0577f8da 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_range.ts @@ -16,18 +16,20 @@ * specific language governing permissions and limitations * under the License. */ + import { get } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import { convertDateRangeToString, DateRangeKey } from './lib/date_range'; import { BUCKET_TYPES } from './bucket_agg_types'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterDateRange } from './create_filter/date_range'; import { KBN_FIELD_TYPES, fieldFormats } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats, getUiSettings } from '../../../../../../../plugins/data/public/services'; -export { convertDateRangeToString, DateRangeKey }; +import { convertDateRangeToString, DateRangeKey } from './lib/date_range'; +export { convertDateRangeToString, DateRangeKey }; // for BWC const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', { defaultMessage: 'Date Range', @@ -41,7 +43,7 @@ export const dateRangeBucketAgg = new BucketAggType({ return { from, to }; }, getFormat(agg) { - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); const formatter = agg.fieldOwnFormatter( fieldFormats.TEXT_CONTEXT_TYPE, @@ -92,7 +94,7 @@ export const dateRangeBucketAgg = new BucketAggType({ ]); } if (!tz) { - const config = npStart.core.uiSettings; + const config = getUiSettings(); const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); const isDefaultTimezone = config.isDefault('dateFormat:tz'); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts index b52e2d6cfd4df4..80efc0cf92071d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/filter.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { i18n } from '@kbn/i18n'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts index 6eaf788b83c04a..2852f3e4bdf464 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/filters.ts @@ -18,19 +18,21 @@ */ import _ from 'lodash'; -import angular from 'angular'; - import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; + import { createFilterFilters } from './create_filter/filters'; +import { toAngularJSON } from '../utils'; import { BucketAggType } from './_bucket_agg_type'; +import { BUCKET_TYPES } from './bucket_agg_types'; import { Storage } from '../../../../../../../plugins/kibana_utils/public'; + import { getQueryLog, esQuery, Query } from '../../../../../../../plugins/data/public'; -import { BUCKET_TYPES } from './bucket_agg_types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getUiSettings } from '../../../../../../../plugins/data/public/services'; const config = chrome.getUiSettingsClient(); -const storage = new Storage(window.localStorage); const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', { defaultMessage: 'Filters', @@ -52,15 +54,17 @@ export const filtersBucketAgg = new BucketAggType({ params: [ { name: 'filters', + // TODO need to get rid of reference to `config` below default: [{ input: { query: '', language: config.get('search:queryLanguage') }, label: '' }], write(aggConfig, output) { + const uiSettings = getUiSettings(); const inFilters: FilterValue[] = aggConfig.params.filters; if (!_.size(inFilters)) return; inFilters.forEach(filter => { const persistedLog = getQueryLog( - config, - storage, + uiSettings, + new Storage(window.localStorage), 'vis_default_editor', filter.input.language ); @@ -77,7 +81,13 @@ export const filtersBucketAgg = new BucketAggType({ return; } - const query = esQuery.buildEsQuery(aggConfig.getIndexPattern(), [input], [], config); + const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); + const query = esQuery.buildEsQuery( + aggConfig.getIndexPattern(), + [input], + [], + esQueryConfigs + ); if (!query) { console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console @@ -90,7 +100,7 @@ export const filtersBucketAgg = new BucketAggType({ matchAllLabel || (typeof filter.input.query === 'string' ? filter.input.query - : angular.toJson(filter.input.query)); + : toAngularJSON(filter.input.query)); filters[label] = { query }; }, {} diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts index f0ad5954764867..09dd03c759155e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -19,12 +19,13 @@ import { geoHashBucketAgg } from './geo_hash'; import { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketAggConfig } from './_bucket_agg_type'; -jest.mock('ui/new_platform'); - describe('Geohash Agg', () => { + // const typesRegistry = mockAggTypesRegistry([geoHashBucketAgg]); + const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (params?: Record) => { const indexPattern = { id: '1234', @@ -62,7 +63,7 @@ describe('Geohash Agg', () => { }, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts index 57e8f6e8c5ded4..9142a30338163c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_tile.ts @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; -import { AggConfigOptions } from '../agg_config'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -57,7 +56,7 @@ export const geoTileBucketAgg = new BucketAggType({ aggs.push(agg); if (useGeocentroid) { - const aggConfig: AggConfigOptions = { + const aggConfig = { type: METRIC_TYPES.GEO_CENTROID, enabled: true, params: { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts index 4e89d7db1ff647..11dc8e42fd6538 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.test.ts @@ -17,16 +17,23 @@ * under the License. */ -import { npStart } from 'ui/new_platform'; -import { AggConfigs } from '../index'; +import { AggConfigs } from '../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, histogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './_bucket_agg_type'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setUiSettings } from '../../../../../../../plugins/data/public/services'; describe('Histogram Agg', () => { - const getAggConfigs = (params: Record = {}) => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry([histogramBucketAgg]); + + const getAggConfigs = (params: Record) => { const indexPattern = { id: '1234', title: 'logstash-*', @@ -45,16 +52,13 @@ describe('Histogram Agg', () => { indexPattern, [ { - field: { - name: 'field', - }, id: 'test', type: BUCKET_TYPES.HISTOGRAM, schema: 'segment', params, }, ], - null + { typesRegistry } ); }; @@ -158,10 +162,15 @@ describe('Histogram Agg', () => { aggConfig.setAutoBounds(autoBounds); } - // mock histogram:maxBars value; - npStart.core.uiSettings.get = jest.fn(() => maxBars as any); + const core = coreMock.createStart(); + setUiSettings({ + ...core.uiSettings, + get: () => maxBars as any, + }); - return aggConfig.write(aggConfigs).params; + const interval = aggConfig.write(aggConfigs).params; + setUiSettings(core.uiSettings); // clean up + return interval; }; it('will respect the histogram:maxBars setting', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts index f7e9ef45961e04..70df2f230db094 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/histogram.ts @@ -19,13 +19,13 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; -import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getNotifications, getUiSettings } from '../../../../../../../plugins/data/public/services'; export interface AutoBounds { min: number; @@ -37,8 +37,6 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { getAutoBounds: () => AutoBounds; } -const getUIConfig = () => npStart.core.uiSettings; - export const histogramBucketAgg = new BucketAggType({ name: BUCKET_TYPES.HISTOGRAM, title: i18n.translate('data.search.aggs.buckets.histogramTitle', { @@ -116,7 +114,7 @@ export const histogramBucketAgg = new BucketAggType({ }) .catch((e: Error) => { if (e.name === 'AbortError') return; - toastNotifications.addWarning( + getNotifications().toasts.addWarning( i18n.translate('data.search.aggs.histogram.missingMaxMinValuesWarning', { defaultMessage: 'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.', @@ -136,7 +134,7 @@ export const histogramBucketAgg = new BucketAggType({ const range = autoBounds.max - autoBounds.min; const bars = range / interval; - const config = getUIConfig(); + const config = getUiSettings(); if (bars > config.get('histogram:maxBars')) { const minInterval = range / config.get('histogram:maxBars'); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts index 91bdf53e7f8091..3fb464d8fa7a8b 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/ip_range.ts @@ -19,15 +19,17 @@ import { noop, map, omit, isNull } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import { IpRangeKey, convertIPRangeToString } from './lib/ip_range'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; -// @ts-ignore import { createFilterIpRange } from './create_filter/ip_range'; import { KBN_FIELD_TYPES, fieldFormats } from '../../../../../../../plugins/data/public'; -export { IpRangeKey, convertIPRangeToString }; + +import { IpRangeKey, convertIPRangeToString } from './lib/ip_range'; +export { IpRangeKey, convertIPRangeToString }; // for BWC + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', { defaultMessage: 'IPv4 Range', @@ -44,7 +46,7 @@ export const ipRangeBucketAgg = new BucketAggType({ return { type: 'range', from: bucket.from, to: bucket.to }; }, getFormat(agg) { - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); const formatter = agg.fieldOwnFormatter( fieldFormats.TEXT_CONTEXT_TYPE, fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.IP) diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts index 77e84e044de55a..d94477b588f8d1 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts @@ -19,10 +19,10 @@ import { isString, isObject } from 'lodash'; import { IBucketAggConfig, BucketAggType, BucketAggParam } from './_bucket_agg_type'; -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; export const isType = (type: string) => { - return (agg: AggConfig): boolean => { + return (agg: IAggConfig): boolean => { const field = agg.params.field; return field && field.type === type; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts index b1b0c4bc30a58e..096b19fe7de66d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/range.test.ts @@ -17,12 +17,12 @@ * under the License. */ +import { rangeBucketAgg } from './range'; import { AggConfigs } from '../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { FieldFormatsGetConfigFn, fieldFormats } from '../../../../../../../plugins/data/public'; -jest.mock('ui/new_platform'); - const buckets = [ { to: 1024, @@ -44,6 +44,12 @@ const buckets = [ ]; describe('Range Agg', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry([rangeBucketAgg]); + const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -80,7 +86,7 @@ describe('Range Agg', () => { }, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts index 37b829bfc20fb2..cee3ed506c29c4 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/significant_terms.test.ts @@ -17,17 +17,16 @@ * under the License. */ -import { AggConfigs } from '../index'; -import { IAggConfigs } from '../types'; +import { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { significantTermsBucketAgg } from './significant_terms'; import { IBucketAggConfig } from './_bucket_agg_type'; -jest.mock('ui/new_platform'); - describe('Significant Terms Agg', () => { describe('order agg editor UI', () => { describe('convert include/exclude from old format', () => { + const typesRegistry = mockAggTypesRegistry([significantTermsBucketAgg]); const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', @@ -53,7 +52,7 @@ describe('Significant Terms Agg', () => { params, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts index 24ac332ae4d55c..9a4f28afd3edf2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.test.ts @@ -17,13 +17,13 @@ * under the License. */ -import { AggConfigs } from '../index'; +import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -jest.mock('ui/new_platform'); - describe('Terms Agg', () => { describe('order agg editor UI', () => { + const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', @@ -48,7 +48,7 @@ describe('Terms Agg', () => { type: BUCKET_TYPES.TERMS, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts index cc1288d339692f..0de1c31d02f961 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts @@ -19,13 +19,12 @@ import { IndexPattern } from '../../../../../../../plugins/data/public'; import { AggTypeFilters } from './agg_type_filters'; -import { AggConfig } from '..'; -import { IAggType } from '../types'; +import { IAggConfig, IAggType } from '../types'; describe('AggTypeFilters', () => { let registry: AggTypeFilters; const indexPattern = ({ id: '1234', fields: [], title: 'foo' } as unknown) as IndexPattern; - const aggConfig = {} as AggConfig; + const aggConfig = {} as IAggConfig; beforeEach(() => { registry = new AggTypeFilters(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts index d3b38ce041d7ee..13a4cc0856b090 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { IndexPattern } from 'src/plugins/data/public'; import { IAggConfig, IAggType } from '../types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts index 431e1161e0dbdf..32cda7b950e93d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/prop_filter.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import expect from '@kbn/expect'; import { propFilter } from './prop_filter'; describe('prop filter', () => { @@ -47,48 +46,48 @@ describe('prop filter', () => { it('returns list when no filters are provided', () => { const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects)).to.eql(objects); + expect(nameFilter(objects)).toEqual(objects); }); it('returns list when empty list of filters is provided', () => { const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects, [])).to.eql(objects); + expect(nameFilter(objects, [])).toEqual(objects); }); it('should keep only the tables', () => { const objects = getObjects('table', 'table', 'pie'); - expect(nameFilter(objects, 'table')).to.eql(getObjects('table', 'table')); + expect(nameFilter(objects, 'table')).toEqual(getObjects('table', 'table')); }); it('should support comma-separated values', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, 'table,line')).to.eql(getObjects('table', 'line')); + expect(nameFilter(objects, 'table,line')).toEqual(getObjects('table', 'line')); }); it('should support an array of values', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, ['table', 'line'])).to.eql(getObjects('table', 'line')); + expect(nameFilter(objects, ['table', 'line'])).toEqual(getObjects('table', 'line')); }); it('should return all objects', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, '*')).to.eql(objects); + expect(nameFilter(objects, '*')).toEqual(objects); }); it('should allow negation', () => { const objects = getObjects('table', 'line', 'pie'); - expect(nameFilter(objects, ['!line'])).to.eql(getObjects('table', 'pie')); + expect(nameFilter(objects, ['!line'])).toEqual(getObjects('table', 'pie')); }); it('should support a function for specifying what should be kept', () => { const objects = getObjects('table', 'line', 'pie'); const line = (value: string) => value === 'line'; - expect(nameFilter(objects, line)).to.eql(getObjects('line')); + expect(nameFilter(objects, line)).toEqual(getObjects('line')); }); it('gracefully handles a filter function with zero arity', () => { const objects = getObjects('table', 'line', 'pie'); const rejectEverything = () => false; - expect(nameFilter(objects, rejectEverything)).to.eql([]); + expect(nameFilter(objects, rejectEverything)).toEqual([]); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.test.ts b/src/legacy/core_plugins/data/public/search/aggs/index.test.ts index a867769a77fc1d..4d0cd55b09d533 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.test.ts @@ -25,8 +25,6 @@ import { isMetricAggType } from './metrics/metric_agg_type'; const bucketAggs = aggTypes.buckets; const metricAggs = aggTypes.metrics; -jest.mock('ui/new_platform'); - describe('AggTypesComponent', () => { describe('bucket aggs', () => { it('all extend BucketAggType', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index 0bdb92b8de65e8..f6914c36f6c05c 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -17,8 +17,13 @@ * under the License. */ -export { aggTypes } from './agg_types'; +export { + AggTypesRegistry, + AggTypesRegistrySetup, + AggTypesRegistryStart, +} from './agg_types_registry'; export { AggType } from './agg_type'; +export { aggTypes } from './agg_types'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; export { FieldParamType } from './param_types'; @@ -52,4 +57,4 @@ export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; // types -export { IAggConfig, IAggConfigs } from './types'; +export { CreateAggConfigParams, IAggConfig, IAggConfigs } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts index 9fb28f8631bc63..11bb5592747297 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_avg.ts @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; - import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts index 83837f0de51146..0668a9bcf57a8f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_max.ts @@ -18,7 +18,6 @@ */ import { i18n } from '@kbn/i18n'; - import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts index d96197693dc2ee..8f728cb5e7e426 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/bucket_min.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts index 147e9255210887..4f7b6e555ca33e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/cardinality.ts @@ -18,10 +18,11 @@ */ import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; const uniqueCountTitle = i18n.translate('data.search.aggs.metrics.uniqueCountTitle', { defaultMessage: 'Unique Count', @@ -37,7 +38,7 @@ export const cardinalityMetricAgg = new MetricAggType({ }); }, getFormat() { - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); return fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts index 14a9bd073ff2bc..8b3e0a488c68a1 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/count.ts @@ -18,10 +18,11 @@ */ import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; export const countMetricAgg = new MetricAggType({ name: METRIC_TYPES.COUNT, @@ -35,7 +36,7 @@ export const countMetricAgg = new MetricAggType({ }); }, getFormat() { - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); return fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts index 054543de3dd067..00d866e6f2b3ed 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { assign } from 'lodash'; import { IMetricAggConfig } from '../metric_agg_type'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index e24aca08271c72..88549ee3019ee6 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -23,7 +23,6 @@ import { noop, identity } from 'lodash'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; - import { Schemas } from '../../schemas'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index e7c98e575fdb4f..05e009cc9da30e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -21,7 +21,6 @@ import { identity } from 'lodash'; import { i18n } from '@kbn/i18n'; import { siblingPipelineAggWriter } from './sibling_pipeline_agg_writer'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; - import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { Schemas } from '../../schemas'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts index 4755a873e69779..ad55837ec9a300 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.test.ts @@ -17,15 +17,16 @@ * under the License. */ +import { medianMetricAgg } from './median'; import { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -jest.mock('ui/new_platform'); - describe('AggTypeMetricMedianProvider class', () => { let aggConfigs: IAggConfigs; beforeEach(() => { + const typesRegistry = mockAggTypesRegistry([medianMetricAgg]); const field = { name: 'bytes', }; @@ -50,7 +51,7 @@ describe('AggTypeMetricMedianProvider class', () => { }, }, ], - null + { typesRegistry } ); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts index 53a5ffff418f10..68fc98261118c2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/median.ts @@ -16,12 +16,10 @@ * specific language governing permissions and limitations * under the License. */ + import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; - -// @ts-ignore -import { percentilesMetricAgg } from './percentiles'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; const medianTitle = i18n.translate('data.search.aggs.metrics.medianTitle', { diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts index 3bae7b92618dcd..952dcc96de8330 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts @@ -18,13 +18,14 @@ */ import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; +import { FilterFieldTypes } from '../param_types/field'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -import { FilterFieldTypes } from '../param_types/field'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; export interface IMetricAggConfig extends AggConfig { type: InstanceType; @@ -78,7 +79,7 @@ export class MetricAggType { - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); const field = agg.getField(); return field ? field.format diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts index 48851051634356..1806c6d9d77107 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/min.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index 11fc39c20bdc42..58b4ee530a8c2d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -17,12 +17,12 @@ * under the License. */ -import sinon from 'sinon'; import { derivativeMetricAgg } from './derivative'; import { cumulativeSumMetricAgg } from './cumulative_sum'; import { movingAvgMetricAgg } from './moving_avg'; import { serialDiffMetricAgg } from './serial_diff'; import { AggConfigs } from '../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; jest.mock('../schemas', () => { @@ -34,9 +34,13 @@ jest.mock('../schemas', () => { }; }); -jest.mock('ui/new_platform'); - describe('parent pipeline aggs', function() { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry(); + const metrics = [ { name: 'derivative', title: 'Derivative', provider: derivativeMetricAgg }, { name: 'cumulative_sum', title: 'Cumulative Sum', provider: cumulativeSumMetricAgg }, @@ -94,7 +98,7 @@ describe('parent pipeline aggs', function() { schema: 'metric', }, ], - null + { typesRegistry } ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) @@ -220,16 +224,16 @@ describe('parent pipeline aggs', function() { }); const searchSource: any = {}; - const customMetricSpy = sinon.spy(); + const customMetricSpy = jest.fn(); const customMetric = aggConfig.params.customMetric; // Attach a modifyAggConfigOnSearchRequestStart with a spy to the first parameter customMetric.type.params[0].modifyAggConfigOnSearchRequestStart = customMetricSpy; aggConfig.type.params.forEach(param => { - param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource); + param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource, {}); }); - expect(customMetricSpy.calledWith(customMetric, searchSource)).toBe(true); + expect(customMetricSpy.mock.calls[0]).toEqual([customMetric, searchSource, {}]); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index 655e918ce07deb..628f1cd204ee5e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -19,14 +19,16 @@ import { IPercentileRanksAggConfig, percentileRanksMetricAgg } from './percentile_ranks'; import { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -jest.mock('ui/new_platform'); - describe('AggTypesMetricsPercentileRanksProvider class', function() { let aggConfigs: IAggConfigs; beforeEach(() => { + mockDataServices(); + + const typesRegistry = mockAggTypesRegistry([percentileRanksMetricAgg]); const field = { name: 'bytes', }; @@ -58,7 +60,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { }, }, ], - null + { typesRegistry } ); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts index 38b47a7e97d2f2..1d640a9c1fa42f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentile_ranks.ts @@ -18,20 +18,17 @@ */ import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; - import { getPercentileValue } from './percentiles_get_value'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormats, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; // required by the values editor - export type IPercentileRanksAggConfig = IResponseAggConfig; -const getFieldFormats = () => npStart.plugins.data.fieldFormats; - const valueProps = { makeLabel(this: IPercentileRanksAggConfig) { const fieldFormatsService = getFieldFormats(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts index dd1aaca973e473..e077bc0f8c7737 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -19,14 +19,14 @@ import { IPercentileAggConfig, percentilesMetricAgg } from './percentiles'; import { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -jest.mock('ui/new_platform'); - describe('AggTypesMetricsPercentilesProvider class', () => { let aggConfigs: IAggConfigs; beforeEach(() => { + const typesRegistry = mockAggTypesRegistry([percentilesMetricAgg]); const field = { name: 'bytes', }; @@ -58,7 +58,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { }, }, ], - null + { typesRegistry } ); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts index 39dc0d0f181e92..49e927d07d8dd0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/percentiles.ts @@ -18,15 +18,11 @@ */ import { i18n } from '@kbn/i18n'; - import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; - import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; - -// @ts-ignore import { ordinalSuffix } from './lib/ordinal_suffix'; export type IPercentileAggConfig = IResponseAggConfig; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index d643cf0d2a4781..d3456bacceb6ab 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { spy } from 'sinon'; import { bucketSumMetricAgg } from './bucket_sum'; import { bucketAvgMetricAgg } from './bucket_avg'; import { bucketMinMetricAgg } from './bucket_min'; @@ -25,6 +24,7 @@ import { bucketMaxMetricAgg } from './bucket_max'; import { AggConfigs } from '../agg_configs'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; jest.mock('../schemas', () => { class MockedSchemas { @@ -35,9 +35,13 @@ jest.mock('../schemas', () => { }; }); -jest.mock('ui/new_platform'); - describe('sibling pipeline aggs', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry(); + const metrics = [ { name: 'sum_bucket', title: 'Overall Sum', provider: bucketSumMetricAgg }, { name: 'avg_bucket', title: 'Overall Average', provider: bucketAvgMetricAgg }, @@ -96,7 +100,7 @@ describe('sibling pipeline aggs', () => { }, }, ], - null + { typesRegistry } ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) @@ -162,8 +166,8 @@ describe('sibling pipeline aggs', () => { init(); const searchSource: any = {}; - const customMetricSpy = spy(); - const customBucketSpy = spy(); + const customMetricSpy = jest.fn(); + const customBucketSpy = jest.fn(); const { customMetric, customBucket } = aggConfig.params; // Attach a modifyAggConfigOnSearchRequestStart with a spy to the first parameter @@ -171,11 +175,11 @@ describe('sibling pipeline aggs', () => { customBucket.type.params[0].modifyAggConfigOnSearchRequestStart = customBucketSpy; aggConfig.type.params.forEach(param => { - param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource); + param.modifyAggConfigOnSearchRequestStart(aggConfig, searchSource, {}); }); - expect(customMetricSpy.calledWith(customMetric, searchSource)).toBe(true); - expect(customBucketSpy.calledWith(customBucket, searchSource)).toBe(true); + expect(customMetricSpy.mock.calls[0]).toEqual([customMetric, searchSource, {}]); + expect(customBucketSpy.mock.calls[0]).toEqual([customBucket, searchSource, {}]); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts index 3125026a521854..0679831b1e6ac8 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/std_deviation.test.ts @@ -19,11 +19,11 @@ import { IStdDevAggConfig, stdDeviationMetricAgg } from './std_deviation'; import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -jest.mock('ui/new_platform'); - describe('AggTypeMetricStandardDeviationProvider class', () => { + const typesRegistry = mockAggTypesRegistry([stdDeviationMetricAgg]); const getAggConfigs = (customLabel?: string) => { const field = { name: 'memory', @@ -52,7 +52,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { }, }, ], - null + { typesRegistry } ); }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts index a973de4fe8659e..ad1f42f5c563e6 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.test.ts @@ -20,11 +20,10 @@ import { dropRight, last } from 'lodash'; import { topHitMetricAgg } from './top_hit'; import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig } from './metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -jest.mock('ui/new_platform'); - describe('Top hit metric', () => { let aggDsl: Record; let aggConfig: IMetricAggConfig; @@ -37,6 +36,7 @@ describe('Top hit metric', () => { fieldType = KBN_FIELD_TYPES.NUMBER, size = 1, }: any) => { + const typesRegistry = mockAggTypesRegistry([topHitMetricAgg]); const field = { name: fieldName, displayName: fieldName, @@ -81,7 +81,7 @@ describe('Top hit metric', () => { params, }, ], - null + { typesRegistry } ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts index 2e7c11004b472e..d31abe64491d0d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts @@ -17,10 +17,10 @@ * under the License. */ -import { AggConfig } from '../agg_config'; +import { AggConfig, IAggConfig } from '../agg_config'; import { BaseParamType } from './base'; -export class AggParamType extends BaseParamType< +export class AggParamType extends BaseParamType< TAggConfig > { makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts index 1523cb03eb966c..95ad71a616ab27 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/base.ts @@ -18,10 +18,10 @@ */ import { IAggConfigs } from '../agg_configs'; -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; import { FetchOptions, ISearchSource } from '../../../../../../../plugins/data/public'; -export class BaseParamType { +export class BaseParamType { name: string; type: string; displayName: string; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts index fa88754ac60b94..7338c41f920d72 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts @@ -25,8 +25,6 @@ import { IAggConfig } from '../agg_config'; import { IMetricAggConfig } from '../metrics/metric_agg_type'; import { Schema } from '../schemas'; -jest.mock('ui/new_platform'); - describe('Field', () => { const indexPattern = { id: '1234', diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts index 40c30f6210a833..bb5707cbb482ec 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { isFunction } from 'lodash'; -import { npStart } from 'ui/new_platform'; import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; @@ -30,6 +29,8 @@ import { indexPatterns, KBN_FIELD_TYPES, } from '../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getNotifications } from '../../../../../../../plugins/data/public/services'; const filterByType = propFilter('type'); @@ -93,7 +94,7 @@ export class FieldParamType extends BaseParamType { // @ts-ignore const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName); if (!validField) { - npStart.core.notifications.toasts.addDanger( + getNotifications().toasts.addDanger( i18n.translate( 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts index bc36bb46d3d166..1a453a225797db 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts @@ -17,27 +17,26 @@ * under the License. */ -import { IndexedArray } from 'ui/indexed_array'; import { AggTypeFieldFilters } from './field_filters'; -import { AggConfig } from '../../agg_config'; +import { IAggConfig } from '../../agg_config'; import { IndexPatternField } from '../../../../../../../../plugins/data/public'; describe('AggTypeFieldFilters', () => { let registry: AggTypeFieldFilters; - const aggConfig = {} as AggConfig; + const aggConfig = {} as IAggConfig; beforeEach(() => { registry = new AggTypeFieldFilters(); }); it('should filter nothing without registered filters', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; const filtered = registry.filter(fields, aggConfig); expect(filtered).toEqual(fields); }); it('should pass all fields to the registered filter', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; const filter = jest.fn(); registry.addFilter(filter); registry.filter(fields, aggConfig); @@ -46,7 +45,7 @@ describe('AggTypeFieldFilters', () => { }); it('should allow registered filters to filter out fields', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; let filtered = registry.filter(fields, aggConfig); expect(filtered).toEqual(fields); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts index 7d1348ab5423be..1cbf0c9ae36245 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts @@ -17,9 +17,9 @@ * under the License. */ import { IndexPatternField } from 'src/plugins/data/public'; -import { AggConfig } from '../../agg_config'; +import { IAggConfig } from '../../agg_config'; -type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: AggConfig) => boolean; +type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: IAggConfig) => boolean; /** * A registry to store {@link AggTypeFieldFilter} which are used to filter down @@ -41,11 +41,11 @@ class AggTypeFieldFilters { /** * Returns the {@link any|fields} filtered by all registered filters. * - * @param fields An IndexedArray of fields that will be filtered down by this registry. + * @param fields An array of fields that will be filtered down by this registry. * @param aggConfig The aggConfig for which the returning list will be used. * @return A filtered list of the passed fields. */ - public filter(fields: IndexPatternField[], aggConfig: AggConfig) { + public filter(fields: IndexPatternField[], aggConfig: IAggConfig) { const allFilters = Array.from(this.filters); const allowedAggTypeFields = fields.filter(field => { const isAggTypeFieldAllowed = allFilters.every(filter => filter(field, aggConfig)); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts index 827299814c62aa..12fd29b3a14527 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.test.ts @@ -19,13 +19,11 @@ import { BaseParamType } from './base'; import { JsonParamType } from './json'; -import { AggConfig } from '../agg_config'; - -jest.mock('ui/new_platform'); +import { IAggConfig } from '../agg_config'; describe('JSON', function() { const paramName = 'json_test'; - let aggConfig: AggConfig; + let aggConfig: IAggConfig; let output: Record; const initAggParam = (config: Record = {}) => @@ -36,7 +34,7 @@ describe('JSON', function() { }); beforeEach(function() { - aggConfig = { params: {} } as AggConfig; + aggConfig = { params: {} } as IAggConfig; output = { params: {} }; }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts index 771919b0bb56b9..bf85b3b890c358 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/json.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; import { BaseParamType } from './base'; export class JsonParamType extends BaseParamType { @@ -29,7 +29,7 @@ export class JsonParamType extends BaseParamType { this.name = config.name || 'json'; if (!config.write) { - this.write = (aggConfig: AggConfig, output: Record) => { + this.write = (aggConfig: IAggConfig, output: Record) => { let paramJson; const param = aggConfig.params[this.name]; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts index 6b58d81914097c..c03d6cdfa1c700 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.test.ts @@ -20,8 +20,6 @@ import { BaseParamType } from './base'; import { OptionedParamType } from './optioned'; -jest.mock('ui/new_platform'); - describe('Optioned', () => { describe('constructor', () => { it('it is an instance of BaseParamType', () => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts index 5ffda3740af49f..9eb7ceda607117 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/optioned.ts @@ -17,14 +17,14 @@ * under the License. */ -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; import { BaseParamType } from './base'; export interface OptionedValueProp { value: string; text: string; disabled?: boolean; - isCompatible: (agg: AggConfig) => boolean; + isCompatible: (agg: IAggConfig) => boolean; } export interface OptionedParamEditorProps { @@ -40,7 +40,7 @@ export class OptionedParamType extends BaseParamType { super(config); if (!config.write) { - this.write = (aggConfig: AggConfig, output: Record) => { + this.write = (aggConfig: IAggConfig, output: Record) => { output.params[this.name] = aggConfig.params[this.name].value; }; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts index fd5ccebde993eb..29ec9741611a37 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.test.ts @@ -19,13 +19,11 @@ import { BaseParamType } from './base'; import { StringParamType } from './string'; -import { AggConfig } from '../agg_config'; - -jest.mock('ui/new_platform'); +import { IAggConfig } from '../agg_config'; describe('String', function() { let paramName = 'json_test'; - let aggConfig: AggConfig; + let aggConfig: IAggConfig; let output: Record; const initAggParam = (config: Record = {}) => @@ -36,7 +34,7 @@ describe('String', function() { }); beforeEach(() => { - aggConfig = { params: {} } as AggConfig; + aggConfig = { params: {} } as IAggConfig; output = { params: {} }; }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts index 58ba99f8a6d636..750606eb8433bf 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/string.ts @@ -17,7 +17,7 @@ * under the License. */ -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; import { BaseParamType } from './base'; export class StringParamType extends BaseParamType { @@ -25,7 +25,7 @@ export class StringParamType extends BaseParamType { super(config); if (!config.write) { - this.write = (aggConfig: AggConfig, output: Record) => { + this.write = (aggConfig: IAggConfig, output: Record) => { if (aggConfig.params[this.name] && aggConfig.params[this.name].length) { output.params[this.name] = aggConfig.params[this.name]; } diff --git a/src/legacy/ui/public/vis/__tests__/index.js b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/index.ts similarity index 86% rename from src/legacy/ui/public/vis/__tests__/index.js rename to src/legacy/core_plugins/data/public/search/aggs/test_helpers/index.ts index 46074f2c5197b7..131f921586144c 100644 --- a/src/legacy/ui/public/vis/__tests__/index.js +++ b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/index.ts @@ -17,5 +17,5 @@ * under the License. */ -import './_agg_config'; -import './_agg_configs'; +export { mockAggTypesRegistry } from './mock_agg_types_registry'; +export { mockDataServices } from './mock_data_services'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts new file mode 100644 index 00000000000000..d6bb7938664932 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.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 { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry'; +import { aggTypes } from '../agg_types'; +import { BucketAggType } from '../buckets/_bucket_agg_type'; +import { MetricAggType } from '../metrics/metric_agg_type'; + +/** + * Testing utility which creates a new instance of AggTypesRegistry, + * registers the provided agg types, and returns AggTypesRegistry.start() + * + * This is useful if your test depends on a certain agg type to be present + * in the registry. + * + * @param [types] - Optional array of AggTypes to register. + * If no value is provided, all default types will be registered. + * + * @internal + */ +export function mockAggTypesRegistry | MetricAggType>( + types?: T[] +): AggTypesRegistryStart { + const registry = new AggTypesRegistry(); + const registrySetup = registry.setup(); + + if (types) { + types.forEach(type => { + if (type instanceof BucketAggType) { + registrySetup.registerBucket(type); + } else if (type instanceof MetricAggType) { + registrySetup.registerMetric(type); + } + }); + } else { + aggTypes.buckets.forEach(type => registrySetup.registerBucket(type)); + aggTypes.metrics.forEach(type => registrySetup.registerMetric(type)); + } + + return registry.start(); +} diff --git a/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts new file mode 100644 index 00000000000000..c4e78ab8f64226 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/test_helpers/mock_data_services.ts @@ -0,0 +1,54 @@ +/* + * 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 { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../plugins/data/public/mocks'; +import { searchStartMock } from '../../mocks'; +import { setSearchServiceShim } from '../../../services'; +import { + setFieldFormats, + setIndexPatterns, + setNotifications, + setOverlays, + setQueryService, + setSearchService, + setUiSettings, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../plugins/data/public/services'; + +/** + * Testing helper which calls all of the service setters used in the + * data plugin. Services are added using their provided mocks. + * + * @internal + */ +export function mockDataServices() { + const core = coreMock.createStart(); + const data = dataPluginMock.createStartContract(); + const searchShim = searchStartMock(); + + setSearchServiceShim(searchShim); + setFieldFormats(data.fieldFormats); + setIndexPatterns(data.indexPatterns); + setNotifications(core.notifications); + setOverlays(core.overlays); + setQueryService(data.query); + setSearchService(data.search); + setUiSettings(core.uiSettings); +} diff --git a/src/legacy/core_plugins/data/public/search/aggs/types.ts b/src/legacy/core_plugins/data/public/search/aggs/types.ts index 2c918abf99fcaf..5d02f426b58967 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/types.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/types.ts @@ -18,7 +18,7 @@ */ export { IAggConfig } from './agg_config'; -export { IAggConfigs } from './agg_configs'; +export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; export { IAggType } from './agg_type'; export { AggParam, AggParamOption } from './agg_params'; export { IFieldParamType } from './param_types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx b/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx index a3c7f24f3927d8..c0662c98755a3d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx +++ b/src/legacy/core_plugins/data/public/search/aggs/utils.test.tsx @@ -19,8 +19,6 @@ import { isValidJson } from './utils'; -jest.mock('ui/new_platform'); - const input = { valid: '{ "test": "json input" }', invalid: 'strings are not json', diff --git a/src/legacy/core_plugins/data/public/search/aggs/utils.ts b/src/legacy/core_plugins/data/public/search/aggs/utils.ts index 62f07ce44ab46e..67ea373f438fb8 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/utils.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/utils.ts @@ -26,7 +26,7 @@ import { isValidEsInterval } from '../../../common'; * @param {string} value a string that should be validated * @returns {boolean} true if value is a valid JSON or if value is an empty string, or a string with whitespaces, otherwise false */ -function isValidJson(value: string): boolean { +export function isValidJson(value: string): boolean { if (!value || value.length === 0) { return true; } @@ -49,7 +49,7 @@ function isValidJson(value: string): boolean { } } -function isValidInterval(value: string, baseInterval?: string) { +export function isValidInterval(value: string, baseInterval?: string) { if (baseInterval) { return _parseWithBase(value, baseInterval); } else { @@ -69,4 +69,37 @@ function _parseWithBase(value: string, baseInterval: string) { } } -export { isValidJson, isValidInterval }; +// An inlined version of angular.toJSON() +// source: https://github.com/angular/angular.js/blob/master/src/Angular.js#L1312 +// @internal +export function toAngularJSON(obj: any, pretty?: any): string { + if (obj === undefined) return ''; + if (typeof pretty === 'number') { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); +} + +function isWindow(obj: any) { + return obj && obj.window === obj; +} + +function isScope(obj: any) { + return obj && obj.$evalAsync && obj.$watch; +} + +function toJsonReplacer(key: any, value: any) { + let val = value; + + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + + return val; +} 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 7a5d927d0f219a..24dd1c4944bfba 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -19,7 +19,7 @@ import { get, has } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { AggConfigs, IAggConfigs } from 'ui/agg_types'; +import { createAggConfigs, IAggConfigs } from 'ui/agg_types'; import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { KibanaContext, @@ -258,7 +258,7 @@ export const esaggs = (): ExpressionFunctionDefinition { - const aggConfigs = new AggConfigs(indexPattern); + const { aggs } = getSearchServiceShim(); + const aggConfigs = aggs.createAggConfigs(indexPattern); const aggConfig = aggConfigs.createAggConfig({ enabled: true, type, diff --git a/src/legacy/core_plugins/data/public/search/mocks.ts b/src/legacy/core_plugins/data/public/search/mocks.ts new file mode 100644 index 00000000000000..86b6a928dc5b42 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/mocks.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 { SearchSetup, SearchStart } from './search_service'; +import { AggTypesRegistrySetup, AggTypesRegistryStart } from './aggs/agg_types_registry'; +import { AggConfigs } from './aggs/agg_configs'; +import { mockAggTypesRegistry } from './aggs/test_helpers'; + +const aggTypeBaseParamMock = () => ({ + name: 'some_param', + type: 'some_param_type', + displayName: 'some_agg_type_param', + required: false, + advanced: false, + default: {}, + write: jest.fn(), + serialize: jest.fn().mockImplementation(() => {}), + deserialize: jest.fn().mockImplementation(() => {}), + options: [], +}); + +const aggTypeConfigMock = () => ({ + name: 'some_name', + title: 'some_title', + params: [aggTypeBaseParamMock()], +}); + +export const aggTypesRegistrySetupMock = (): MockedKeys => ({ + registerBucket: jest.fn(), + registerMetric: jest.fn(), +}); + +export const aggTypesRegistryStartMock = (): MockedKeys => ({ + get: jest.fn().mockImplementation(aggTypeConfigMock), + getBuckets: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), + getMetrics: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), + getAll: jest.fn().mockImplementation(() => ({ + buckets: [aggTypeConfigMock()], + metrics: [aggTypeConfigMock()], + })), +}); + +export const searchSetupMock = (): MockedKeys => ({ + aggs: { + types: aggTypesRegistrySetupMock(), + }, +}); + +export const searchStartMock = (): MockedKeys => ({ + aggs: { + createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { + return new AggConfigs(indexPattern, configStates, { + schemas, + typesRegistry: mockAggTypesRegistry(), + }); + }), + types: mockAggTypesRegistry(), + __LEGACY: { + AggConfig: jest.fn() as any, + AggType: jest.fn(), + aggTypeFieldFilters: jest.fn() as any, + FieldParamType: jest.fn(), + MetricAggType: jest.fn(), + parentPipelineAggHelper: jest.fn() as any, + setBounds: jest.fn(), + siblingPipelineAggHelper: jest.fn() as any, + }, + }, +}); diff --git a/src/legacy/core_plugins/data/public/search/search_service.ts b/src/legacy/core_plugins/data/public/search/search_service.ts index 45f9ff17328adb..6754c0e3551af5 100644 --- a/src/legacy/core_plugins/data/public/search/search_service.ts +++ b/src/legacy/core_plugins/data/public/search/search_service.ts @@ -18,11 +18,16 @@ */ import { CoreSetup, CoreStart } from '../../../../../core/public'; +import { IndexPattern } from '../../../../../plugins/data/public'; import { aggTypes, AggType, + AggTypesRegistry, + AggTypesRegistrySetup, + AggTypesRegistryStart, AggConfig, AggConfigs, + CreateAggConfigParams, FieldParamType, MetricAggType, aggTypeFieldFilters, @@ -32,20 +37,28 @@ import { } from './aggs'; interface AggsSetup { - types: typeof aggTypes; + types: AggTypesRegistrySetup; } -interface AggsStart { - types: typeof aggTypes; +interface AggsStartLegacy { AggConfig: typeof AggConfig; - AggConfigs: typeof AggConfigs; AggType: typeof AggType; aggTypeFieldFilters: typeof aggTypeFieldFilters; FieldParamType: typeof FieldParamType; MetricAggType: typeof MetricAggType; parentPipelineAggHelper: typeof parentPipelineAggHelper; - siblingPipelineAggHelper: typeof siblingPipelineAggHelper; setBounds: typeof setBounds; + siblingPipelineAggHelper: typeof siblingPipelineAggHelper; +} + +interface AggsStart { + createAggConfigs: ( + indexPattern: IndexPattern, + configStates?: CreateAggConfigParams[], + schemas?: Record + ) => InstanceType; + types: AggTypesRegistryStart; + __LEGACY: AggsStartLegacy; } export interface SearchSetup { @@ -63,28 +76,41 @@ export interface SearchStart { * it will move into the existing search service in src/plugins/data/public/search */ export class SearchService { + private readonly aggTypesRegistry = new AggTypesRegistry(); + public setup(core: CoreSetup): SearchSetup { + const aggTypesSetup = this.aggTypesRegistry.setup(); + aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b)); + aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m)); + return { aggs: { - types: aggTypes, // TODO convert to registry - // TODO add other items as needed + types: aggTypesSetup, }, }; } public start(core: CoreStart): SearchStart { + const aggTypesStart = this.aggTypesRegistry.start(); return { aggs: { - types: aggTypes, // TODO convert to registry - AggConfig, // TODO make static - AggConfigs, - AggType, - aggTypeFieldFilters, - FieldParamType, - MetricAggType, - parentPipelineAggHelper, // TODO make static - siblingPipelineAggHelper, // TODO make static - setBounds, // TODO make static + createAggConfigs: (indexPattern, configStates = [], schemas) => { + return new AggConfigs(indexPattern, configStates, { + schemas, + typesRegistry: aggTypesStart, + }); + }, + types: aggTypesStart, + __LEGACY: { + AggConfig, // TODO make static + AggType, + aggTypeFieldFilters, + FieldParamType, + MetricAggType, + parentPipelineAggHelper, // TODO make static + setBounds, // TODO make static + siblingPipelineAggHelper, // TODO make static + }, }, }; } diff --git a/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts index ef2748102623ab..98048cb25db2fe 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts @@ -20,8 +20,6 @@ import { TabifyBuckets } from './buckets'; import { AggGroupNames } from '../aggs'; -jest.mock('ui/new_platform'); - describe('Buckets wrapper', () => { const check = (aggResp: any, count: number, keys: string[]) => { test('reads the length', () => { diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts index cfd4cd7de640b2..6c5dc790ef9767 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -18,12 +18,17 @@ */ import { tabifyGetColumns } from './get_columns'; -import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; import { TabbedAggColumn } from './types'; - -jest.mock('ui/new_platform'); +import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; +import { mockAggTypesRegistry, mockDataServices } from '../aggs/test_helpers'; describe('get columns', () => { + beforeEach(() => { + mockDataServices(); + }); + + const typesRegistry = mockAggTypesRegistry(); + const createAggConfigs = (aggs: any[] = []) => { const field = { name: '@timestamp', @@ -38,18 +43,17 @@ describe('get columns', () => { }, } as any; - return new AggConfigs( - indexPattern, - aggs, - new Schemas([ + return new AggConfigs(indexPattern, aggs, { + typesRegistry, + schemas: new Schemas([ { group: AggGroupNames.Metrics, name: 'metric', min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, - ]).all - ); + ]).all, + }); }; test('should inject a count metric if no aggs exist', () => { diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts index f5df0a683ca00c..94301eedac74a7 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -19,14 +19,19 @@ import { TabbedAggResponseWriter } from './response_writer'; import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; +import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; import { TabbedResponseWriterOptions } from './types'; -jest.mock('ui/new_platform'); - describe('TabbedAggResponseWriter class', () => { + beforeEach(() => { + mockDataServices(); + }); + let responseWriter: TabbedAggResponseWriter; + const typesRegistry = mockAggTypesRegistry(); + const splitAggConfig = [ { type: BUCKET_TYPES.TERMS, @@ -66,18 +71,17 @@ describe('TabbedAggResponseWriter class', () => { } as any; return new TabbedAggResponseWriter( - new AggConfigs( - indexPattern, - aggs, - new Schemas([ + new AggConfigs(indexPattern, aggs, { + typesRegistry, + schemas: new Schemas([ { group: AggGroupNames.Metrics, name: 'metric', min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, - ]).all - ), + ]).all, + }), { metricsAtAllLevels: false, partialRows: false, diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts index 13fe7719b0a856..db4ad3bdea96bd 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -20,11 +20,12 @@ import { IndexPattern } from '../../../../../../plugins/data/public'; import { tabifyAggResponse } from './tabify'; import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; +import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; -jest.mock('ui/new_platform'); - describe('tabifyAggResponse Integration', () => { + const typesRegistry = mockAggTypesRegistry(); + const createAggConfigs = (aggs: IAggConfig[] = []) => { const field = { name: '@timestamp', @@ -39,18 +40,17 @@ describe('tabifyAggResponse Integration', () => { }, } as unknown) as IndexPattern; - return new AggConfigs( - indexPattern, - aggs, - new Schemas([ + return new AggConfigs(indexPattern, aggs, { + typesRegistry, + schemas: new Schemas([ { group: AggGroupNames.Metrics, name: 'metric', min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, - ]).all - ); + ]).all, + }); }; const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts b/src/legacy/core_plugins/data/public/services.ts similarity index 76% rename from src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts rename to src/legacy/core_plugins/data/public/services.ts index 2cecfd0fe8b767..7ecd041c70e22e 100644 --- a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/services.ts @@ -17,12 +17,9 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../../core/public/mocks'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; +import { SearchStart } from './search/search_service'; -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); +export const [getSearchServiceShim, setSearchServiceShim] = createGetterSetter( + 'searchShim' +); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index 6591aa5fb53d5f..6ae4e415f8caa8 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -20,7 +20,8 @@ import { cloneDeep } from 'lodash'; import { Vis, VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { AggConfigs, IAggConfig, AggGroupNames } from '../../../legacy_imports'; + +import { createAggConfigs, IAggConfig, AggGroupNames } from '../../../legacy_imports'; import { EditorStateActionTypes } from './constants'; import { getEnabledMetricAggsCount } from '../../agg_group_helper'; import { EditorAction } from './actions'; @@ -32,7 +33,8 @@ function initEditorState(vis: Vis) { function editorStateReducer(state: VisState, action: EditorAction): VisState { switch (action.type) { case EditorStateActionTypes.ADD_NEW_AGG: { - const aggConfig = state.aggs.createAggConfig(action.payload as IAggConfig, { + const payloadAggConfig = action.payload as IAggConfig; + const aggConfig = state.aggs.createAggConfig(payloadAggConfig, { addToAggConfigs: false, }); aggConfig.brandNew = true; @@ -40,7 +42,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -63,7 +65,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -88,7 +90,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -129,7 +131,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -141,7 +143,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } @@ -163,7 +165,7 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { return { ...state, - aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + aggs: createAggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), }; } diff --git a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts index 832f73752a99b6..8aed263c4e4d1d 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts @@ -22,12 +22,12 @@ export { AggType, IAggType, IAggConfig, - AggConfigs, IAggConfigs, AggParam, AggGroupNames, aggGroupNamesMap, aggTypes, + createAggConfigs, FieldParamType, IFieldParamType, BUCKET_TYPES, diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts index 0e1e48d00a1b27..736152c7014dc2 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts @@ -34,7 +34,7 @@ import { stubFields } from '../../../../plugins/data/public/stubs'; import { tableVisResponseHandler } from './table_vis_response_handler'; import { coreMock } from '../../../../core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AggConfigs } from 'ui/agg_types'; +import { createAggConfigs } from 'ui/agg_types'; import { tabifyAggResponse, IAggConfig } from './legacy_imports'; jest.mock('ui/new_platform'); @@ -113,7 +113,7 @@ describe('Table Vis - Controller', () => { return ({ type: tableVisTypeDefinition, params: Object.assign({}, tableVisTypeDefinition.visConfig.defaults, params), - aggs: new AggConfigs( + aggs: createAggConfigs( stubIndexPattern, [ { type: 'count', schema: 'metric' }, diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index fb7a157b53a9ac..0a3b1938436c0e 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -18,10 +18,10 @@ */ export { - AggConfigs, IAggConfig, IAggConfigs, isDateHistogramBucketAggConfig, setBounds, } from '../../data/public'; +export { createAggConfigs } from 'ui/agg_types'; export { createSavedSearchesLoader } from '../../../../plugins/discover/public'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js index 2f36322c67256a..15a826cc6ddbe5 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js @@ -30,7 +30,7 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; import { PersistedState } from '../../../../../../../src/plugins/visualizations/public'; -import { AggConfigs } from '../../legacy_imports'; +import { createAggConfigs } from '../../legacy_imports'; import { updateVisualizationConfig } from './legacy/vis_update'; import { getTypes } from './services'; @@ -83,7 +83,7 @@ class VisImpl extends EventEmitter { updateVisualizationConfig(state.params, this.params); if (state.aggs || !this.aggs) { - this.aggs = new AggConfigs( + this.aggs = createAggConfigs( this.indexPattern, state.aggs ? state.aggs.aggs || state.aggs : [], this.type.schemas.all @@ -125,7 +125,7 @@ class VisImpl extends EventEmitter { copyCurrentState(includeDisabled = false) { const state = this.getCurrentState(includeDisabled); - state.aggs = new AggConfigs( + state.aggs = createAggConfigs( this.indexPattern, state.aggs.aggs || state.aggs, this.type.schemas.all diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ac5d0bed7ef153..ffc300251c4bb3 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -27,18 +27,19 @@ import { start as dataStart } from '../../../core_plugins/data/public/legacy'; // runtime contracts +const { types } = dataStart.search.aggs; +export const aggTypes = types.getAll(); +export const { createAggConfigs } = dataStart.search.aggs; export const { - types: aggTypes, AggConfig, - AggConfigs, AggType, aggTypeFieldFilters, FieldParamType, MetricAggType, parentPipelineAggHelper, - siblingPipelineAggHelper, setBounds, -} = dataStart.search.aggs; + siblingPipelineAggHelper, +} = dataStart.search.aggs.__LEGACY; // types export { diff --git a/src/legacy/ui/public/vis/__tests__/_agg_config.js b/src/legacy/ui/public/vis/__tests__/_agg_config.js deleted file mode 100644 index 9e53044f681baa..00000000000000 --- a/src/legacy/ui/public/vis/__tests__/_agg_config.js +++ /dev/null @@ -1,485 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { AggType, AggConfig } from '../../agg_types'; -import { start as visualizationsStart } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; - -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('AggConfig', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - describe('#toDsl', function() { - it('calls #write()', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }); - - const aggConfig = vis.aggs.byName('date_histogram')[0]; - const stub = sinon.stub(aggConfig, 'write').returns({ params: {} }); - - aggConfig.toDsl(); - expect(stub.callCount).to.be(1); - }); - - it('uses the type name as the agg name', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }); - - const aggConfig = vis.aggs.byName('date_histogram')[0]; - sinon.stub(aggConfig, 'write').returns({ params: {} }); - - const dsl = aggConfig.toDsl(); - expect(dsl).to.have.property('date_histogram'); - }); - - it('uses the params from #write() output as the agg params', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }); - - const aggConfig = vis.aggs.byName('date_histogram')[0]; - const football = {}; - - sinon.stub(aggConfig, 'write').returns({ params: football }); - - const dsl = aggConfig.toDsl(); - expect(dsl.date_histogram).to.be(football); - }); - - it('includes subAggs from #write() output', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'avg', - schema: 'metric', - }, - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }); - - const histoConfig = vis.aggs.byName('date_histogram')[0]; - const avgConfig = vis.aggs.byName('avg')[0]; - const football = {}; - - sinon.stub(histoConfig, 'write').returns({ params: {}, subAggs: [avgConfig] }); - sinon.stub(avgConfig, 'write').returns({ params: football }); - - const dsl = histoConfig.toDsl(); - - // didn't use .eql() because of variable key names, and final check is strict - expect(dsl).to.have.property('aggs'); - expect(dsl.aggs).to.have.property(avgConfig.id); - expect(dsl.aggs[avgConfig.id]).to.have.property('avg'); - expect(dsl.aggs[avgConfig.id].avg).to.be(football); - }); - }); - - describe('::ensureIds', function() { - it('accepts an array of objects and assigns ids to them', function() { - const objs = [{}, {}, {}, {}]; - AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', '1'); - expect(objs[1]).to.have.property('id', '2'); - expect(objs[2]).to.have.property('id', '3'); - expect(objs[3]).to.have.property('id', '4'); - }); - - it('assigns ids relative to the other only item in the list', function() { - const objs = [{ id: '100' }, {}]; - AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', '100'); - expect(objs[1]).to.have.property('id', '101'); - }); - - it('assigns ids relative to the other items in the list', function() { - const objs = [{ id: '100' }, { id: '200' }, { id: '500' }, { id: '350' }, {}]; - AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', '100'); - expect(objs[1]).to.have.property('id', '200'); - expect(objs[2]).to.have.property('id', '500'); - expect(objs[3]).to.have.property('id', '350'); - expect(objs[4]).to.have.property('id', '501'); - }); - - it('uses ::nextId to get the starting value', function() { - sinon.stub(AggConfig, 'nextId').returns(534); - const objs = AggConfig.ensureIds([{}]); - AggConfig.nextId.restore(); - expect(objs[0]).to.have.property('id', '534'); - }); - - it('only calls ::nextId once', function() { - const start = 420; - sinon.stub(AggConfig, 'nextId').returns(start); - const objs = AggConfig.ensureIds([{}, {}, {}, {}, {}, {}, {}]); - - expect(AggConfig.nextId).to.have.property('callCount', 1); - - AggConfig.nextId.restore(); - objs.forEach(function(obj, i) { - expect(obj).to.have.property('id', String(start + i)); - }); - }); - }); - - describe('::nextId', function() { - it('accepts a list of objects and picks the next id', function() { - const next = AggConfig.nextId([{ id: 100 }, { id: 500 }]); - expect(next).to.be(501); - }); - - it('handles an empty list', function() { - const next = AggConfig.nextId([]); - expect(next).to.be(1); - }); - - it('fails when the list is not defined', function() { - expect(function() { - AggConfig.nextId(); - }).to.throwError(); - }); - }); - - describe('#toJsonDataEquals', function() { - const testsIdentical = [ - { - type: 'metric', - aggs: [ - { - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - ], - }, - { - type: 'histogram', - aggs: [ - { - type: 'avg', - schema: 'metric', - }, - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }, - ]; - - testsIdentical.forEach((visConfig, index) => { - it(`identical aggregations (${index})`, function() { - const vis1 = new visualizationsStart.Vis(indexPattern, visConfig); - const vis2 = new visualizationsStart.Vis(indexPattern, visConfig); - expect(vis1.aggs.jsonDataEquals(vis2.aggs.aggs)).to.be(true); - }); - }); - - const testsIdenticalDifferentOrder = [ - { - config1: { - type: 'histogram', - aggs: [ - { - type: 'avg', - schema: 'metric', - }, - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }, - config2: { - type: 'histogram', - aggs: [ - { - schema: 'metric', - type: 'avg', - }, - { - schema: 'segment', - type: 'date_histogram', - }, - ], - }, - }, - ]; - - testsIdenticalDifferentOrder.forEach((test, index) => { - it(`identical aggregations (${index}) - init json is in different order`, function() { - const vis1 = new visualizationsStart.Vis(indexPattern, test.config1); - const vis2 = new visualizationsStart.Vis(indexPattern, test.config2); - expect(vis1.aggs.jsonDataEquals(vis2.aggs.aggs)).to.be(true); - }); - }); - - const testsDifferent = [ - { - config1: { - type: 'histogram', - aggs: [ - { - type: 'avg', - schema: 'metric', - }, - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }, - config2: { - type: 'histogram', - aggs: [ - { - type: 'max', - schema: 'metric', - }, - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }, - }, - { - config1: { - type: 'metric', - aggs: [ - { - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - ], - }, - config2: { - type: 'metric', - aggs: [ - { - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }, - }, - ]; - - testsDifferent.forEach((test, index) => { - it(`different aggregations (${index})`, function() { - const vis1 = new visualizationsStart.Vis(indexPattern, test.config1); - const vis2 = new visualizationsStart.Vis(indexPattern, test.config2); - expect(vis1.aggs.jsonDataEquals(vis2.aggs.aggs)).to.be(false); - }); - }); - }); - - describe('#toJSON', function() { - it('includes the aggs id, params, type and schema', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }); - - const aggConfig = vis.aggs.byName('date_histogram')[0]; - expect(aggConfig.id).to.be('1'); - expect(aggConfig.params).to.be.an('object'); - expect(aggConfig.type) - .to.be.an(AggType) - .and.have.property('name', 'date_histogram'); - expect(aggConfig.schema) - .to.be.an('object') - .and.have.property('name', 'segment'); - - const state = aggConfig.toJSON(); - expect(state).to.have.property('id', '1'); - expect(state.params).to.be.an('object'); - expect(state).to.have.property('type', 'date_histogram'); - expect(state).to.have.property('schema', 'segment'); - }); - - it('test serialization order is identical (for visual consistency)', function() { - const vis1 = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - }, - ], - }); - const vis2 = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - schema: 'segment', - type: 'date_histogram', - }, - ], - }); - - //this relies on the assumption that js-engines consistently loop over properties in insertion order. - //most likely the case, but strictly speaking not guaranteed by the JS and JSON specifications. - expect(JSON.stringify(vis1.aggs.aggs) === JSON.stringify(vis2.aggs.aggs)).to.be(true); - }); - }); - - describe('#makeLabel', function() { - it('uses the custom label if it is defined', function() { - const vis = new visualizationsStart.Vis(indexPattern, {}); - const aggConfig = vis.aggs.aggs[0]; - aggConfig.params.customLabel = 'Custom label'; - const label = aggConfig.makeLabel(); - expect(label).to.be(aggConfig.params.customLabel); - }); - it('default label should be "Count"', function() { - const vis = new visualizationsStart.Vis(indexPattern, {}); - const aggConfig = vis.aggs.aggs[0]; - const label = aggConfig.makeLabel(); - expect(label).to.be('Count'); - }); - it('default label should be "Percentage of Count" when percentageMode is set to true', function() { - const vis = new visualizationsStart.Vis(indexPattern, {}); - const aggConfig = vis.aggs.aggs[0]; - const label = aggConfig.makeLabel(true); - expect(label).to.be('Percentage of Count'); - }); - it('empty label if the visualizationsStart.Vis type is not defined', function() { - const vis = new visualizationsStart.Vis(indexPattern, {}); - const aggConfig = vis.aggs.aggs[0]; - aggConfig.type = undefined; - const label = aggConfig.makeLabel(); - expect(label).to.be(''); - }); - }); - - describe('#fieldFormatter - custom getFormat handler', function() { - it('returns formatter from getFormat handler', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'metric', - aggs: [ - { - type: 'count', - schema: 'metric', - params: { field: '@timestamp' }, - }, - ], - }); - - const fieldFormatter = vis.aggs.aggs[0].fieldFormatter(); - - expect(fieldFormatter).to.be.defined; - expect(fieldFormatter('text')).to.be('text'); - }); - }); - - describe('#fieldFormatter - no custom getFormat handler', function() { - const visStateAggWithoutCustomGetFormat = { - aggs: [ - { - type: 'histogram', - schema: 'bucket', - params: { field: 'bytes' }, - }, - ], - }; - let vis; - - beforeEach(function() { - vis = new visualizationsStart.Vis(indexPattern, visStateAggWithoutCustomGetFormat); - }); - - it("returns the field's formatter", function() { - expect(vis.aggs.aggs[0].fieldFormatter().toString()).to.be( - vis.aggs.aggs[0] - .getField() - .format.getConverterFor() - .toString() - ); - }); - - it('returns the string format if the field does not have a format', function() { - const agg = vis.aggs.aggs[0]; - agg.params.field = { type: 'number', format: null }; - const fieldFormatter = agg.fieldFormatter(); - expect(fieldFormatter).to.be.defined; - expect(fieldFormatter('text')).to.be('text'); - }); - - it('returns the string format if their is no field', function() { - const agg = vis.aggs.aggs[0]; - delete agg.params.field; - const fieldFormatter = agg.fieldFormatter(); - expect(fieldFormatter).to.be.defined; - expect(fieldFormatter('text')).to.be('text'); - }); - - it('returns the html converter if "html" is passed in', function() { - const field = indexPattern.fields.getByName('bytes'); - expect(vis.aggs.aggs[0].fieldFormatter('html').toString()).to.be( - field.format.getConverterFor('html').toString() - ); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/__tests__/_agg_configs.js b/src/legacy/ui/public/vis/__tests__/_agg_configs.js deleted file mode 100644 index 172523ec50c8b0..00000000000000 --- a/src/legacy/ui/public/vis/__tests__/_agg_configs.js +++ /dev/null @@ -1,420 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { AggConfig, AggConfigs, AggGroupNames, Schemas } from '../../agg_types'; -import { start as visualizationsStart } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('AggConfigs', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - // load main deps - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - describe('constructor', function() { - it('handles passing just a vis', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [], - }); - - const ac = new AggConfigs(vis.indexPattern, [], vis.type.schemas.all); - expect(ac.aggs).to.have.length(1); - }); - - it('converts configStates into AggConfig objects if they are not already', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [], - }); - - const ac = new AggConfigs( - vis.indexPattern, - [ - { - type: 'date_histogram', - schema: 'segment', - }, - new AggConfig(vis.aggs, { - type: 'terms', - schema: 'split', - }), - ], - vis.type.schemas.all - ); - - expect(ac.aggs).to.have.length(3); - }); - - it('attempts to ensure that all states have an id', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [], - }); - - const states = [ - { - type: 'date_histogram', - schema: 'segment', - }, - { - type: 'terms', - schema: 'split', - }, - ]; - - const spy = sinon.spy(AggConfig, 'ensureIds'); - new AggConfigs(vis.indexPattern, states, vis.type.schemas.all); - expect(spy.callCount).to.be(1); - expect(spy.firstCall.args[0]).to.be(states); - AggConfig.ensureIds.restore(); - }); - - describe('defaults', function() { - let vis; - beforeEach(function() { - vis = { - indexPattern: indexPattern, - type: { - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: 'Simple', - min: 1, - max: 2, - defaults: [ - { schema: 'metric', type: 'count' }, - { schema: 'metric', type: 'avg' }, - { schema: 'metric', type: 'sum' }, - ], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: 'Example', - min: 0, - max: 1, - defaults: [ - { schema: 'segment', type: 'terms' }, - { schema: 'segment', type: 'filters' }, - ], - }, - ]), - }, - }; - }); - - it('should only set the number of defaults defined by the max', function() { - const ac = new AggConfigs(vis.indexPattern, [], vis.type.schemas.all); - expect(ac.bySchemaName('metric')).to.have.length(2); - }); - - it('should set the defaults defined in the schema when none exist', function() { - const ac = new AggConfigs(vis.indexPattern, [], vis.type.schemas.all); - expect(ac.aggs).to.have.length(3); - }); - - it('should NOT set the defaults defined in the schema when some exist', function() { - const ac = new AggConfigs( - vis.indexPattern, - [{ schema: 'segment', type: 'date_histogram' }], - vis.type.schemas.all - ); - expect(ac.aggs).to.have.length(3); - expect(ac.bySchemaName('segment')[0].type.name).to.equal('date_histogram'); - }); - }); - }); - - describe('#getRequestAggs', function() { - it('performs a stable sort, but moves metrics to the bottom', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'avg', schema: 'metric' }, - { type: 'terms', schema: 'split' }, - { type: 'histogram', schema: 'split' }, - { type: 'sum', schema: 'metric' }, - { type: 'date_histogram', schema: 'segment' }, - { type: 'filters', schema: 'split' }, - { type: 'percentiles', schema: 'metric' }, - ], - }); - - const sorted = vis.aggs.getRequestAggs(); - const aggs = _.indexBy(vis.aggs.aggs, function(agg) { - return agg.type.name; - }); - - expect(sorted.shift()).to.be(aggs.terms); - expect(sorted.shift()).to.be(aggs.histogram); - expect(sorted.shift()).to.be(aggs.date_histogram); - expect(sorted.shift()).to.be(aggs.filters); - expect(sorted.shift()).to.be(aggs.avg); - expect(sorted.shift()).to.be(aggs.sum); - expect(sorted.shift()).to.be(aggs.percentiles); - expect(sorted).to.have.length(0); - }); - }); - - describe('#getResponseAggs', function() { - it('returns all request aggs for basic aggs', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'split' }, - { type: 'date_histogram', schema: 'segment' }, - { type: 'count', schema: 'metric' }, - ], - }); - - const sorted = vis.aggs.getResponseAggs(); - const aggs = _.indexBy(vis.aggs.aggs, function(agg) { - return agg.type.name; - }); - - expect(sorted.shift()).to.be(aggs.terms); - expect(sorted.shift()).to.be(aggs.date_histogram); - expect(sorted.shift()).to.be(aggs.count); - expect(sorted).to.have.length(0); - }); - - it('expands aggs that have multiple responses', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'split' }, - { type: 'date_histogram', schema: 'segment' }, - { type: 'percentiles', schema: 'metric', params: { percents: [1, 2, 3] } }, - ], - }); - - const sorted = vis.aggs.getResponseAggs(); - const aggs = _.indexBy(vis.aggs.aggs, function(agg) { - return agg.type.name; - }); - - expect(sorted.shift()).to.be(aggs.terms); - expect(sorted.shift()).to.be(aggs.date_histogram); - expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 1); - expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 2); - expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 3); - expect(sorted).to.have.length(0); - }); - }); - - describe('#toDsl', function() { - it('uses the sorted aggs', function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram' }); - sinon.spy(vis.aggs, 'getRequestAggs'); - vis.aggs.toDsl(); - expect(vis.aggs.getRequestAggs).to.have.property('callCount', 1); - }); - - it('calls aggConfig#toDsl() on each aggConfig and compiles the nested output', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'date_histogram', schema: 'segment' }, - { type: 'filters', schema: 'split' }, - ], - }); - - const aggInfos = vis.aggs.aggs.map(function(aggConfig) { - const football = {}; - - sinon.stub(aggConfig, 'toDsl').returns(football); - - return { - id: aggConfig.id, - football: football, - }; - }); - - (function recurse(lvl) { - const info = aggInfos.shift(); - - expect(lvl).to.have.property(info.id); - expect(lvl[info.id]).to.be(info.football); - - if (lvl[info.id].aggs) { - return recurse(lvl[info.id].aggs); - } - })(vis.aggs.toDsl()); - - expect(aggInfos).to.have.length(1); - }); - - it("skips aggs that don't have a dsl representation", function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'count', schema: 'metric' }, - ], - }); - - const dsl = vis.aggs.toDsl(); - const histo = vis.aggs.byName('date_histogram')[0]; - const count = vis.aggs.byName('count')[0]; - - expect(dsl).to.have.property(histo.id); - expect(dsl[histo.id]).to.be.an('object'); - expect(dsl[histo.id]).to.not.have.property('aggs'); - expect(dsl).to.not.have.property(count.id); - }); - - it('writes multiple metric aggregations at the same level', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { type: 'min', schema: 'metric', params: { field: 'bytes' } }, - { type: 'max', schema: 'metric', params: { field: 'bytes' } }, - ], - }); - - const dsl = vis.aggs.toDsl(); - - const histo = vis.aggs.byName('date_histogram')[0]; - const metrics = vis.aggs.bySchemaGroup('metrics'); - - expect(dsl).to.have.property(histo.id); - expect(dsl[histo.id]).to.be.an('object'); - expect(dsl[histo.id]).to.have.property('aggs'); - - metrics.forEach(function(metric) { - expect(dsl[histo.id].aggs).to.have.property(metric.id); - expect(dsl[histo.id].aggs[metric.id]).to.not.have.property('aggs'); - }); - }); - - it('writes multiple metric aggregations at every level if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'segment', params: { field: 'ip', orderBy: 1 } }, - { type: 'terms', schema: 'segment', params: { field: 'extension', orderBy: 1 } }, - { id: 1, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { type: 'min', schema: 'metric', params: { field: 'bytes' } }, - { type: 'max', schema: 'metric', params: { field: 'bytes' } }, - ], - }); - vis.isHierarchical = _.constant(true); - - const topLevelDsl = vis.aggs.toDsl(vis.isHierarchical()); - const buckets = vis.aggs.bySchemaGroup('buckets'); - const metrics = vis.aggs.bySchemaGroup('metrics'); - - (function checkLevel(dsl) { - const bucket = buckets.shift(); - expect(dsl).to.have.property(bucket.id); - - expect(dsl[bucket.id]).to.be.an('object'); - expect(dsl[bucket.id]).to.have.property('aggs'); - - metrics.forEach(function(metric) { - expect(dsl[bucket.id].aggs).to.have.property(metric.id); - expect(dsl[bucket.id].aggs[metric.id]).to.not.have.property('aggs'); - }); - - if (buckets.length) { - checkLevel(dsl[bucket.id].aggs); - } - })(topLevelDsl); - }); - - it('adds the parent aggs of nested metrics at every level if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - id: '1', - type: 'avg_bucket', - schema: 'metric', - params: { - customBucket: { - id: '1-bucket', - type: 'date_histogram', - schema: 'bucketAgg', - params: { - field: '@timestamp', - interval: '10s', - }, - }, - customMetric: { - id: '1-metric', - type: 'count', - schema: 'metricAgg', - params: {}, - }, - }, - }, - { - id: '2', - type: 'terms', - schema: 'bucket', - params: { - field: 'geo.src', - }, - }, - { - id: '3', - type: 'terms', - schema: 'bucket', - params: { - field: 'machine.os', - }, - }, - ], - }); - vis.isHierarchical = _.constant(true); - - const topLevelDsl = vis.aggs.toDsl(vis.isHierarchical())['2']; - expect(topLevelDsl.aggs).to.have.keys(['1', '1-bucket']); - expect(topLevelDsl.aggs['1'].avg_bucket).to.have.property('buckets_path', '1-bucket>_count'); - expect(topLevelDsl.aggs['3'].aggs).to.have.keys(['1', '1-bucket']); - expect(topLevelDsl.aggs['3'].aggs['1'].avg_bucket).to.have.property( - 'buckets_path', - '1-bucket>_count' - ); - }); - }); -}); diff --git a/src/plugins/data/common/field_formats/mocks.ts b/src/plugins/data/common/field_formats/mocks.ts new file mode 100644 index 00000000000000..bc38374e147cf6 --- /dev/null +++ b/src/plugins/data/common/field_formats/mocks.ts @@ -0,0 +1,49 @@ +/* + * 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 { FieldFormat, IFieldFormatsRegistry } from '.'; + +const fieldFormatMock = ({ + convert: jest.fn(), + getConverterFor: jest.fn(), + getParamDefaults: jest.fn(), + param: jest.fn(), + params: jest.fn(), + toJSON: jest.fn(), + type: jest.fn(), + setupContentType: jest.fn(), +} as unknown) as FieldFormat; + +export const fieldFormatsMock: IFieldFormatsRegistry = { + getByFieldType: jest.fn(), + getDefaultConfig: jest.fn(), + getDefaultInstance: jest.fn().mockImplementation(() => fieldFormatMock) as any, + getDefaultInstanceCacheResolver: jest.fn(), + getDefaultInstancePlain: jest.fn(), + getDefaultType: jest.fn(), + getDefaultTypeName: jest.fn(), + getInstance: jest.fn() as any, + getType: jest.fn(), + getTypeNameByEsTypes: jest.fn(), + init: jest.fn(), + register: jest.fn(), + parseDefaultTypeMap: jest.fn(), + deserialize: jest.fn(), + getTypeWithoutMetaParams: jest.fn(), +}; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 6a0a33096eaac1..27de3b5a29bfd0 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -16,13 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { - Plugin, - DataPublicPluginSetup, - DataPublicPluginStart, - IndexPatternsContract, - IFieldFormatsRegistry, -} from '.'; + +import { Plugin, DataPublicPluginSetup, DataPublicPluginStart, IndexPatternsContract } from '.'; +import { fieldFormatsMock } from '../common/field_formats/mocks'; import { searchSetupMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; @@ -35,24 +31,6 @@ const autocompleteMock: any = { hasQuerySuggestions: jest.fn(), }; -const fieldFormatsMock: IFieldFormatsRegistry = { - getByFieldType: jest.fn(), - getDefaultConfig: jest.fn(), - getDefaultInstance: jest.fn() as any, - getDefaultInstanceCacheResolver: jest.fn(), - getDefaultInstancePlain: jest.fn(), - getDefaultType: jest.fn(), - getDefaultTypeName: jest.fn(), - getInstance: jest.fn() as any, - getType: jest.fn(), - getTypeNameByEsTypes: jest.fn(), - init: jest.fn(), - register: jest.fn(), - parseDefaultTypeMap: jest.fn(), - deserialize: jest.fn(), - getTypeWithoutMetaParams: jest.fn(), -}; - const createSetupContract = (): Setup => { const querySetupMock = queryServiceMock.createSetupContract(); const setupContract = { diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index fd72158012de6f..700bea741bd6ae 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -17,25 +17,6 @@ * under the License. */ -/* - * 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 { ISearchSource } from './search_source'; export const searchSourceMock: MockedKeys = { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index ef4b5f6d7b834f..2e1645c816140c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -17,10 +17,6 @@ jest.mock('ui/new_platform'); // mock away actual dependencies to prevent all of it being loaded jest.mock('../../../../../../src/legacy/core_plugins/interpreter/public/registries', () => {}); -jest.mock('../../../../../../src/legacy/core_plugins/data/public/legacy', () => ({ - start: {}, - setup: {}, -})); jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, })); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index c883983f8cf010..da04b970a494b8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -2803,7 +2803,7 @@ tr:hover .c3:focus::before {
- - - -
- - -
- - - - + - - -
- - - - - - + - + title="Cu0n232QMyvNtzb75j" + > - - Cu0n232QMyvNtzb75j - - + + - - - - - + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + + - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
+ + + + + + + + +
+ + + + + + + +
+ + +
+ +
@@ -3105,7 +3087,7 @@ tr:hover .c3:focus::before {
- - - -
- - -
- - - - + - - -
- - - - - - + - + title="files" + > - - files - - + + - - - - - + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + + - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
+ + + + + + + + +
+ + + + + + + +
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�XQjYjxgHSfU&kein@skwcwquR%t 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