From 8a9ce5a1efe70b740bbad2f39f1f0392036dee7d Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 13 Sep 2021 20:41:06 -0400 Subject: [PATCH 01/39] [Security Solution][Detection Alerts] Fixes alert page refresh issues (#111042) Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../common/components/events_viewer/index.tsx | 19 + .../public/common/store/inputs/selectors.ts | 2 +- .../components/alerts_table/index.tsx | 2 + .../timeline_actions/alert_context_menu.tsx | 43 ++- .../components/take_action_dropdown/index.tsx | 42 +- .../__snapshots__/index.test.tsx.snap | 358 +++++++++++++++++- .../components/t_grid/integrated/index.tsx | 10 +- 7 files changed, 460 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 6bfe61b3eac51..c62337b2426d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -87,6 +87,7 @@ const StatefulEventsViewerComponent: React.FC = ({ entityType, excludedRowRendererIds, filters, + globalQuery, id, isLive, itemsPerPage, @@ -102,6 +103,7 @@ const StatefulEventsViewerComponent: React.FC = ({ scopeId, showCheckboxes, sort, + timelineQuery, utilityBar, additionalFilters, // If truthy, the graph viewer (Resolver) is showing @@ -157,6 +159,18 @@ const StatefulEventsViewerComponent: React.FC = ({ [dispatch, id] ); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + const onAlertStatusActionSuccess = useCallback(() => { + if (id === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [id, timelineQuery, globalQuery]); + const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]); + return ( <> @@ -166,6 +180,7 @@ const StatefulEventsViewerComponent: React.FC = ({ id, type: 'embedded', browserFields, + bulkActions, columns, dataProviders: dataProviders!, defaultCellActions, @@ -245,6 +260,8 @@ const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); const timeline: TimelineModel = getTimeline(state, id) ?? defaultModel; @@ -280,6 +297,8 @@ const makeMapStateToProps = () => { // Used to determine whether the footer should show (since it is hidden if the graph is showing.) // `getTimeline` actually returns `TimelineModel | undefined` graphEventId, + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, id), }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts index f63a9b5be6836..3aedc4696f301 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts @@ -57,7 +57,7 @@ export const globalTimeRangeSelector = createSelector(selectGlobal, (global) => export const globalPolicySelector = createSelector(selectGlobal, (global) => global.policy); -export const globalQuery = createSelector(selectGlobal, (global) => global.queries); +export const globalQuery = () => createSelector(selectGlobal, (global) => global.queries); export const globalQueryByIdSelector = () => createSelector(selectGlobalQuery, (query) => query); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 3c277d1d4019b..89d0fd2e4dbd0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -172,6 +172,7 @@ export const AlertsTableComponent: React.FC = ({ title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); break; case 'acknowledged': + case 'in-progress': title = i18n.ACKNOWLEDGED_ALERT_SUCCESS_TOAST(updated); } displaySuccessToast(title, dispatchToaster); @@ -191,6 +192,7 @@ export const AlertsTableComponent: React.FC = ({ title = i18n.OPENED_ALERT_FAILED_TOAST; break; case 'acknowledged': + case 'in-progress': title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST; } displayErrorToast(title, [error.message], dispatchToaster); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index f2297b7d567bc..3a815468932f0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui'; import { indexOf } from 'lodash'; - +import { connect, ConnectedProps } from 'react-redux'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; @@ -21,7 +21,8 @@ import { AddExceptionModalProps, } from '../../../../common/components/exceptions/add_exception_modal'; import * as i18n from '../translations'; -import { inputsModel } from '../../../../common/store'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; +import { TimelineId } from '../../../../../common'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; @@ -49,7 +50,7 @@ interface AlertContextMenuProps { timelineId: string; } -const AlertContextMenuComponent: React.FC = ({ +const AlertContextMenuComponent: React.FC = ({ ariaLabel = i18n.MORE_ACTIONS, ariaRowindex, columnValues, @@ -58,6 +59,8 @@ const AlertContextMenuComponent: React.FC = ({ refetch, onRuleChange, timelineId, + globalQuery, + timelineQuery, }) => { const [isPopoverOpen, setPopover] = useState(false); @@ -102,6 +105,18 @@ const AlertContextMenuComponent: React.FC = ({ ); }, [disabled, onButtonClick, ariaLabel]); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const refetchAll = useCallback(() => { + if (timelineId === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [timelineId, globalQuery, timelineQuery]); + const { exceptionModalType, onAddExceptionCancel, @@ -110,7 +125,7 @@ const AlertContextMenuComponent: React.FC = ({ ruleIndices, } = useExceptionModal({ ruleIndex: ecsRowData?.signal?.rule?.index, - refetch, + refetch: refetchAll, timelineId, }); @@ -125,7 +140,7 @@ const AlertContextMenuComponent: React.FC = ({ eventId: ecsRowData?._id, indexName: ecsRowData?._index ?? '', timelineId, - refetch, + refetch: refetchAll, closePopover, }); @@ -218,7 +233,23 @@ const AlertContextMenuComponent: React.FC = ({ ); }; -export const AlertContextMenu = React.memo(AlertContextMenuComponent); +const makeMapStateToProps = () => { + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { timelineId }: AlertContextMenuProps) => { + return { + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, timelineId), + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const AlertContextMenu = connector(React.memo(AlertContextMenuComponent)); type AddExceptionModalWrapperProps = Omit< AddExceptionModalProps, diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 0432e7d353086..425d049388764 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -9,7 +9,8 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { isEmpty } from 'lodash/fp'; -import { TimelineEventsDetailsItem } from '../../../../common'; +import { connect, ConnectedProps } from 'react-redux'; +import { TimelineEventsDetailsItem, TimelineId } from '../../../../common'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; @@ -23,6 +24,7 @@ import { Status } from '../../../../common/detection_engine/schemas/common/schem import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_check'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; interface ActionsData { alertStatus: Status; @@ -46,7 +48,7 @@ export interface TakeActionDropdownProps { timelineId: string; } -export const TakeActionDropdown = React.memo( +export const TakeActionDropdownComponent = React.memo( ({ detailsData, ecsData, @@ -59,7 +61,9 @@ export const TakeActionDropdown = React.memo( onAddIsolationStatusClick, refetch, timelineId, - }: TakeActionDropdownProps) => { + globalQuery, + timelineQuery, + }: TakeActionDropdownProps & PropsFromRedux) => { const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -141,12 +145,24 @@ export const TakeActionDropdown = React.memo( closePopoverHandler(); }, [closePopoverHandler]); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const refetchAll = useCallback(() => { + if (timelineId === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [timelineId, globalQuery, timelineQuery]); + const { actionItems: statusActionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, closePopover: closePopoverAndFlyout, eventId: actionsData.eventId, indexName, - refetch, + refetch: refetchAll, timelineId, }); @@ -216,3 +232,21 @@ export const TakeActionDropdown = React.memo( ) : null; } ); + +const makeMapStateToProps = () => { + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { timelineId }: TakeActionDropdownProps) => { + return { + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, timelineId), + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const TakeActionDropdown = connector(React.memo(TakeActionDropdownComponent)); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 1608120095f8d..b2a8a439220c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -1233,7 +1233,7 @@ Array [
- + > + +
@@ -2094,7 +2269,7 @@ Array [
- + > + +
diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 48d4b0098458f..afce668eb04e2 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -19,7 +19,12 @@ import { Direction, EntityType } from '../../../../common/search_strategy'; import type { DocValueFields } from '../../../../common/search_strategy'; import type { CoreStart } from '../../../../../../../src/core/public'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { TGridCellAction, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { + BulkActionsProp, + TGridCellAction, + TimelineId, + TimelineTabs, +} from '../../../../common/types/timeline'; import type { CellValueElementProps, @@ -95,6 +100,7 @@ const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { additionalFilters: React.ReactNode; browserFields: BrowserFields; + bulkActions?: BulkActionsProp; columns: ColumnHeaderOptions[]; data?: DataPublicPluginStart; dataProviders: DataProvider[]; @@ -135,6 +141,7 @@ export interface TGridIntegratedProps { const TGridIntegratedComponent: React.FC = ({ additionalFilters, browserFields, + bulkActions = true, columns, data, dataProviders, @@ -334,6 +341,7 @@ const TGridIntegratedComponent: React.FC = ({ hasAlertsCrud={hasAlertsCrud} activePage={pageInfo.activePage} browserFields={browserFields} + bulkActions={bulkActions} filterQuery={filterQuery} data={nonDeletedEvents} defaultCellActions={defaultCellActions} From fbeefedd98766b4b0aae82a607650a0ed24920a7 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 13 Sep 2021 18:17:43 -0700 Subject: [PATCH 02/39] Make saved queries share-capable (#111462) * Use resolve instead of get for saved query service * Update tests * Update src/plugins/data/public/query/saved_query/saved_query_service.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Revert step 4 * Make saved queries share-capable * Fix test Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- src/plugins/data/server/saved_objects/query.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts index 2e8b80cf3f080..bc6225255b5e6 100644 --- a/src/plugins/data/server/saved_objects/query.ts +++ b/src/plugins/data/server/saved_objects/query.ts @@ -11,7 +11,8 @@ import { SavedObjectsType } from 'kibana/server'; export const querySavedObjectType: SavedObjectsType = { name: 'query', hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', management: { icon: 'search', defaultSearchField: 'title', From d38d7561532dbe8baaf54b35e7ea877d24677070 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Mon, 13 Sep 2021 23:50:30 -0400 Subject: [PATCH 03/39] [Security Solution][Endpoint][Trusted Apps] TA delete modal shows # of policies it will impact (#111481) --- .../trusted_app_deletion_dialog.test.tsx.snap | 156 ++++++++++++++---- .../view/trusted_app_deletion_dialog.tsx | 44 ++++- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 4 files changed, 159 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap index 0343ab62b9773..070f1b9eabe23 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap @@ -42,7 +42,13 @@ exports[`TrustedAppDeletionDialog renders correctly when deletion failed 1`] = `
- Remove trusted application + Delete " + + trusted app 3 + + "
-

- You are removing trusted application " - + - ". -

+ Warning + +
+
+
+

+ Deleting this entry will remove it from all associated policies. +

+
+
+
+
+

This action cannot be undone. Are you sure you wish to continue?

@@ -98,7 +128,7 @@ exports[`TrustedAppDeletionDialog renders correctly when deletion failed 1`] = ` - Remove trusted application + Delete @@ -146,7 +176,13 @@ exports[`TrustedAppDeletionDialog renders correctly when deletion is in progress
- Remove trusted application + Delete " + + trusted app 3 + + "
-

- You are removing trusted application " - + - ". -

+ Warning + +
+
+
+

+ Deleting this entry will remove it from all associated policies. +

+
+
+
+
+

This action cannot be undone. Are you sure you wish to continue?

@@ -207,7 +267,7 @@ exports[`TrustedAppDeletionDialog renders correctly when deletion is in progress - Remove trusted application + Delete @@ -255,7 +315,13 @@ exports[`TrustedAppDeletionDialog renders correctly when dialog started 1`] = `
- Remove trusted application + Delete " + + trusted app 3 + + "
-

- You are removing trusted application " - + - ". -

+ Warning + +
+
+
+

+ Deleting this entry will remove it from all associated policies. +

+
+
+
+
+

This action cannot be undone. Are you sure you wish to continue?

@@ -311,7 +401,7 @@ exports[`TrustedAppDeletionDialog renders correctly when dialog started 1`] = ` - Remove trusted application + Delete diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx index 3afa2642eba12..9e76cfd001c97 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx @@ -14,6 +14,8 @@ import { EuiButtonEmpty, EuiButtonProps, PropsForButton, + EuiCallOut, + EuiSpacer, EuiModal, EuiModalBody, EuiModalFooter, @@ -22,7 +24,13 @@ import { EuiText, } from '@elastic/eui'; -import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; +import { + Immutable, + ImmutableObject, + PolicyEffectScope, + GlobalEffectScope, + TrustedApp, +} from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; import { useTrustedAppsSelector } from './hooks'; import { @@ -33,19 +41,36 @@ import { const CANCEL_SUBJ = 'trustedAppDeletionCancel'; const CONFIRM_SUBJ = 'trustedAppDeletionConfirm'; +const isTrustedAppByPolicy = ( + trustedApp: ImmutableObject +): trustedApp is ImmutableObject => { + return (trustedApp as ImmutableObject).policies !== undefined; +}; const getTranslations = (entry: Immutable | undefined) => ({ title: ( {entry?.name} }} /> ), - mainMessage: ( + calloutTitle: ( {entry?.name} }} + id="xpack.securitySolution.trustedapps.deletionDialog.calloutTitle" + defaultMessage="Warning" + /> + ), + calloutMessage: ( + ), subMessage: ( @@ -63,7 +88,7 @@ const getTranslations = (entry: Immutable | undefined) => ({ confirmButton: ( ), }); @@ -105,8 +130,11 @@ export const TrustedAppDeletionDialog = memo(() => { + +

{translations.calloutMessage}

+
+ -

{translations.mainMessage}

{translations.subMessage}

diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d1069c2d7dbf8..61e89e8b37161 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23386,9 +23386,7 @@ "xpack.securitySolution.trustedapps.creationSuccess.title": "成功!", "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "キャンセル", "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "信頼できるアプリケーションを削除", - "xpack.securitySolution.trustedapps.deletionDialog.mainMessage": "信頼できるアプリケーション「{name}」を削除しています。", "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "この操作は元に戻すことができません。続行していいですか?", - "xpack.securitySolution.trustedapps.deletionDialog.title": "信頼できるアプリケーションを削除", "xpack.securitySolution.trustedapps.deletionError.text": "信頼できるアプリケーションリストから「{name}」を削除できません。理由:{message}", "xpack.securitySolution.trustedapps.deletionError.title": "削除失敗", "xpack.securitySolution.trustedapps.deletionSuccess.text": "「{name}」は信頼できるアプリケーションリストから削除されました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ed5a47395794c..a9859b8618efd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23767,9 +23767,7 @@ "xpack.securitySolution.trustedapps.creationSuccess.title": "成功!", "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "取消", "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "移除受信任的应用程序", - "xpack.securitySolution.trustedapps.deletionDialog.mainMessage": "您正在移除受信任的应用程序“{name}”。", "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "此操作无法撤消。是否确定要继续?", - "xpack.securitySolution.trustedapps.deletionDialog.title": "移除受信任的应用程序", "xpack.securitySolution.trustedapps.deletionError.text": "无法从受信任的应用程序列表中移除“{name}”。原因:{message}", "xpack.securitySolution.trustedapps.deletionError.title": "移除失败", "xpack.securitySolution.trustedapps.deletionSuccess.text": "“{name}”已从受信任的应用程序列表中移除。", From 43ea2930cc6941018766ab71c2d6bed2af45192f Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Tue, 14 Sep 2021 10:32:38 +0200 Subject: [PATCH 04/39] Decode file name on upload value lists and fix bug with removing value list (#111838) * Decode fileName when creating a list * Return wait_for for delete list item * Return back import * Update x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts Co-authored-by: Ryland Herrick * Use i18n for message Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ryland Herrick --- .../items/write_lines_to_bulk_list_items.test.ts | 16 ++++++++++++++++ .../items/write_lines_to_bulk_list_items.ts | 12 ++++++++---- .../server/services/lists/delete_list.test.ts | 2 +- .../lists/server/services/lists/delete_list.ts | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts index c3856fde9b2c3..78098fde59827 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts @@ -6,6 +6,7 @@ */ import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { LinesResult, @@ -23,6 +24,10 @@ jest.mock('./create_list_items_bulk', () => ({ createListItemsBulk: jest.fn(), })); +jest.mock('../lists/create_list_if_it_does_not_exist', () => ({ + createListIfItDoesNotExist: jest.fn(), +})); + describe('write_lines_to_bulk_list_items', () => { beforeEach(() => { jest.clearAllMocks(); @@ -61,6 +66,17 @@ describe('write_lines_to_bulk_list_items', () => { expect.objectContaining({ value: ['127.0.0.1', '127.0.0.2'] }) ); }); + + it('creates a list with a decoded file name', async () => { + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream({ ...options, listId: undefined }); + options.stream.push(`--\nContent-Disposition: attachment; filename="%22Filename%22.txt"`); + options.stream.push(null); + await promise; + expect(createListIfItDoesNotExist).toBeCalledWith( + expect.objectContaining({ id: `"Filename".txt`, name: `"Filename".txt` }) + ); + }); }); describe('writeBufferToItems', () => { diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 89a6bdbc77878..edd78e350054d 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -17,6 +17,7 @@ import type { Type, } from '@kbn/securitysolution-io-ts-list-types'; import { Version } from '@kbn/securitysolution-io-ts-types'; +import { i18n } from '@kbn/i18n'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { ConfigType } from '../../config'; @@ -59,17 +60,20 @@ export const importListItemsToStream = ({ let list: ListSchema | null = null; readBuffer.on('fileName', async (fileNameEmitted: string) => { readBuffer.pause(); - fileName = fileNameEmitted; + fileName = decodeURIComponent(fileNameEmitted); if (listId == null) { list = await createListIfItDoesNotExist({ - description: `File uploaded from file system of ${fileNameEmitted}`, + description: i18n.translate('xpack.lists.services.items.fileUploadFromFileSystem', { + defaultMessage: 'File uploaded from file system of {fileName}', + values: { fileName }, + }), deserializer, esClient, - id: fileNameEmitted, + id: fileName, immutable: false, listIndex, meta, - name: fileNameEmitted, + name: fileName, serializer, type, user, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index 9ceecbc299bab..f379fd977f51a 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -61,7 +61,7 @@ describe('delete_list', () => { const deleteQuery = { id: LIST_ID, index: LIST_INDEX, - refresh: false, + refresh: 'wait_for', }; expect(options.esClient.delete).toHaveBeenNthCalledWith(1, deleteQuery); }); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index b9a55e107ab76..517723fc227de 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -42,7 +42,7 @@ export const deleteList = async ({ await esClient.delete({ id, index: listIndex, - refresh: false, + refresh: 'wait_for', }); return list; } From 182533f565037b216662f13efdd87409697bede1 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 14 Sep 2021 11:57:42 +0300 Subject: [PATCH 05/39] [Datatable] Removes the old implementation (#111339) * [Datatable] Removes the angular implementation * Fix i18n errors * Fix types * Remove unecessary funtional tests * Fix documentation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- scripts/functional_tests.js | 1 - src/plugins/vis_type_table/README.md | 7 +- src/plugins/vis_type_table/common/index.ts | 5 +- src/plugins/vis_type_table/config.ts | 1 - src/plugins/vis_type_table/kibana.json | 2 +- src/plugins/vis_type_table/public/index.ts | 5 +- .../table_vis_legacy_fn.test.ts.snap | 44 - .../public/legacy/_table_vis.scss | 25 - .../public/legacy/agg_table/_agg_table.scss | 42 - .../public/legacy/agg_table/_index.scss | 1 - .../public/legacy/agg_table/agg_table.html | 34 - .../public/legacy/agg_table/agg_table.js | 253 ------ .../public/legacy/agg_table/agg_table.test.js | 488 ----------- .../legacy/agg_table/agg_table_group.html | 77 -- .../legacy/agg_table/agg_table_group.js | 45 - .../legacy/agg_table/agg_table_group.test.js | 132 --- .../public/legacy/agg_table/tabified_data.js | 784 ------------------ .../public/legacy/get_inner_angular.ts | 88 -- .../vis_type_table/public/legacy/index.scss | 10 - .../vis_type_table/public/legacy/index.ts | 9 - .../public/legacy/paginated_table/_index.scss | 1 - .../paginated_table/_table_cell_filter.scss | 30 - .../paginated_table/paginated_table.html | 55 -- .../legacy/paginated_table/paginated_table.js | 98 --- .../paginated_table/paginated_table.test.ts | 464 ----------- .../public/legacy/paginated_table/rows.js | 130 --- .../paginated_table/table_cell_filter.html | 23 - .../public/legacy/register_legacy_vis.ts | 26 - .../public/legacy/table_vis.html | 29 - .../public/legacy/table_vis_controller.js | 45 - .../legacy/table_vis_controller.test.ts | 245 ------ .../public/legacy/table_vis_legacy_fn.test.ts | 67 -- .../public/legacy/table_vis_legacy_fn.ts | 62 -- .../public/legacy/table_vis_legacy_module.ts | 30 - .../legacy/table_vis_legacy_renderer.tsx | 42 - .../table_vis_legacy_response_handler.ts | 95 --- .../public/legacy/table_vis_legacy_type.ts | 85 -- .../public/legacy/to_ast_legacy.ts | 70 -- .../public/legacy/vis_controller.ts | 121 --- src/plugins/vis_type_table/public/plugin.ts | 32 +- .../vis_type_table/public/register_vis.ts | 6 +- src/plugins/vis_type_table/server/index.ts | 3 - .../apps/visualize/legacy/_data_table.ts | 321 ------- .../functional/apps/visualize/legacy/index.ts | 39 - test/functional/config.legacy.ts | 28 - .../translations/translations/ja-JP.json | 8 +- .../translations/translations/zh-CN.json | 8 +- 47 files changed, 14 insertions(+), 4202 deletions(-) delete mode 100644 src/plugins/vis_type_table/public/legacy/__snapshots__/table_vis_legacy_fn.test.ts.snap delete mode 100644 src/plugins/vis_type_table/public/legacy/_table_vis.scss delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/_index.scss delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js delete mode 100644 src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js delete mode 100644 src/plugins/vis_type_table/public/legacy/get_inner_angular.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/index.scss delete mode 100644 src/plugins/vis_type_table/public/legacy/index.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/rows.js delete mode 100644 src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html delete mode 100644 src/plugins/vis_type_table/public/legacy/register_legacy_vis.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis.html delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_controller.js delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.test.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_legacy_response_handler.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/to_ast_legacy.ts delete mode 100644 src/plugins/vis_type_table/public/legacy/vis_controller.ts delete mode 100644 test/functional/apps/visualize/legacy/_data_table.ts delete mode 100644 test/functional/apps/visualize/legacy/index.ts delete mode 100644 test/functional/config.legacy.ts diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index c541b9faaeccc..ce62face43906 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -13,7 +13,6 @@ const alwaysImportedTests = [ require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.ts'), require.resolve('../test/security_functional/config.ts'), - require.resolve('../test/functional/config.legacy.ts'), ]; // eslint-disable-next-line no-restricted-syntax const onlyNotInCoverageTests = [ diff --git a/src/plugins/vis_type_table/README.md b/src/plugins/vis_type_table/README.md index a17d1142f0c09..b3aa1983fb8ef 100644 --- a/src/plugins/vis_type_table/README.md +++ b/src/plugins/vis_type_table/README.md @@ -1,8 +1,3 @@ Contains the data table visualization, that allows presenting data in a simple table format. -By default a new version of visualization will be used. To use the previous version of visualization the config must have the `vis_type_table.legacyVisEnabled: true` setting -configured in `kibana.dev.yml` or `kibana.yml`, as shown in the example below: - -```yaml -vis_type_table.legacyVisEnabled: true -``` \ No newline at end of file +Aggregation-based datatable visualizations use the EuiDataGrid component. diff --git a/src/plugins/vis_type_table/common/index.ts b/src/plugins/vis_type_table/common/index.ts index 59cfa7e715f3b..ad8e27c95a5ec 100644 --- a/src/plugins/vis_type_table/common/index.ts +++ b/src/plugins/vis_type_table/common/index.ts @@ -6,7 +6,4 @@ * Side Public License, v 1. */ -// TODO: https://github.com/elastic/kibana/issues/110891 -/* eslint-disable @kbn/eslint/no_export_all */ - -export * from './types'; +export { AggTypes, TableVisParams, VIS_TYPE_TABLE } from './types'; diff --git a/src/plugins/vis_type_table/config.ts b/src/plugins/vis_type_table/config.ts index ee027b79d5b5c..b831d26854c30 100644 --- a/src/plugins/vis_type_table/config.ts +++ b/src/plugins/vis_type_table/config.ts @@ -10,7 +10,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - legacyVisEnabled: schema.boolean({ defaultValue: false }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json index 389094802e999..b3ebd5117bbc8 100644 --- a/src/plugins/vis_type_table/kibana.json +++ b/src/plugins/vis_type_table/kibana.json @@ -20,5 +20,5 @@ "name": "Vis Editors", "githubTeam": "kibana-vis-editors" }, - "description": "Registers the datatable aggregation-based visualization. Currently it contains two implementations, the one based on EUI datagrid and the angular one. The second one is going to be removed in future minors." + "description": "Registers the datatable aggregation-based visualization." } diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts index e0ddf6e97e5ce..700a794e5a4c7 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { PluginInitializerContext } from 'kibana/public'; import { TableVisPlugin as Plugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); +export function plugin() { + return new Plugin(); } diff --git a/src/plugins/vis_type_table/public/legacy/__snapshots__/table_vis_legacy_fn.test.ts.snap b/src/plugins/vis_type_table/public/legacy/__snapshots__/table_vis_legacy_fn.test.ts.snap deleted file mode 100644 index a32609c2e3d34..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/__snapshots__/table_vis_legacy_fn.test.ts.snap +++ /dev/null @@ -1,44 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`interpreter/functions#table returns an object with the correct structure 1`] = ` -Object { - "as": "table_vis", - "type": "render", - "value": Object { - "visConfig": Object { - "dimensions": Object { - "buckets": Array [], - "metrics": Array [ - Object { - "accessor": 0, - "aggType": "count", - "format": Object { - "id": "number", - }, - "params": Object {}, - }, - ], - }, - "perPage": 10, - "showMetricsAtAllLevels": false, - "showPartialRows": false, - "showTotal": false, - "sort": Object { - "columnIndex": null, - "direction": null, - }, - "title": "My Chart title", - "totalFunc": "sum", - }, - "visData": Object { - "tables": Array [ - Object { - "columns": Array [], - "rows": Array [], - }, - ], - }, - "visType": "table", - }, -} -`; diff --git a/src/plugins/vis_type_table/public/legacy/_table_vis.scss b/src/plugins/vis_type_table/public/legacy/_table_vis.scss deleted file mode 100644 index 6ae3ddcca81a9..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/_table_vis.scss +++ /dev/null @@ -1,25 +0,0 @@ -// SASSTODO: Update naming to BEM -// This chart is actively being re-written to React and EUI -// Putting off renaming to avoid conflicts -.table-vis { - display: flex; - flex-direction: column; - flex: 1 1 0; - overflow: auto; - - @include euiScrollBar; -} - -.table-vis-container { - kbn-agg-table-group > .table > tbody > tr > td { - border-top: 0; - } - - .pagination-other-pages { - justify-content: flex-end; - } - - .pagination-size { - display: none; - } -} diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss deleted file mode 100644 index 89c7f6664c2fa..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/_agg_table.scss +++ /dev/null @@ -1,42 +0,0 @@ -kbn-agg-table, -kbn-agg-table-group { - display: block; -} - -.kbnAggTable { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.kbnAggTable__paginated { - flex: 1 1 auto; - overflow: auto; - - th { - text-align: left; - font-weight: $euiFontWeightBold; - } - - tr:hover td, - .kbnTableCellFilter { - background-color: $euiColorLightestShade; - } -} - -.kbnAggTable__controls { - flex: 0 0 auto; - display: flex; - align-items: center; - margin: $euiSizeS $euiSizeXS; - - > paginate-controls { - flex: 1 0 auto; - margin: 0; - padding: 0; - } -} - -.small { - font-size: .9em !important; -} diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/_index.scss b/src/plugins/vis_type_table/public/legacy/agg_table/_index.scss deleted file mode 100644 index 340e08a76f1bd..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './agg_table'; diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html deleted file mode 100644 index 5107bd2048286..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.html +++ /dev/null @@ -1,34 +0,0 @@ - - -
-    - - - -     - - - - - -
-
diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js deleted file mode 100644 index 8864326c0edc9..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js +++ /dev/null @@ -1,253 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; -import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../../services'; -import { i18n } from '@kbn/i18n'; - -export function KbnAggTable(config, RecursionHelper) { - return { - restrict: 'E', - template: aggTableTemplate, - scope: { - table: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - controllerAs: 'aggTable', - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el); - }, - controller: function ($scope) { - const self = this; - - self._saveAs = require('@elastic/filesaver').saveAs; - self.csv = { - separator: config.get(CSV_SEPARATOR_SETTING), - quoteValues: config.get(CSV_QUOTE_VALUES_SETTING), - }; - - self.exportAsCsv = function (formatted) { - const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); - self._saveAs(csv, self.csv.filename); - }; - - self.toCsv = function (formatted) { - const rows = $scope.rows; - const columns = $scope.formattedColumns; - - const nonAlphaNumRE = /[^a-zA-Z0-9]/; - const allDoubleQuoteRE = /"/g; - - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; - } - - const csvRows = []; - for (const row of rows) { - const rowArray = []; - for (const col of columns) { - const value = row[col.id]; - const formattedValue = - formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value); - rowArray.push(formattedValue); - } - csvRows.push(rowArray); - } - - // add the columns to the rows - csvRows.unshift(columns.map(({ title }) => escape(title))); - - return csvRows - .map(function (row) { - return row.join(self.csv.separator) + '\r\n'; - }) - .join(''); - }; - - $scope.$watchMulti( - ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], - function () { - const { table, exportTitle, percentageCol } = $scope; - const showPercentage = percentageCol !== ''; - - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - return; - } - - self.csv.filename = (exportTitle || table.title || 'unsaved') + '.csv'; - $scope.rows = table.rows; - $scope.formattedColumns = []; - - if (typeof $scope.dimensions === 'undefined') return; - - const { buckets, metrics } = $scope.dimensions; - - $scope.formattedColumns = table.columns - .map(function (col, i) { - const isBucket = buckets.find((bucket) => bucket.accessor === i); - const dimension = isBucket || metrics.find((metric) => metric.accessor === i); - - const formatter = dimension - ? getFormatService().deserialize(dimension.format) - : undefined; - - const formattedColumn = { - id: col.id, - title: col.name, - formatter: formatter, - filterable: !!isBucket, - }; - - if (!dimension) return; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = - dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = (tableRows) => { - return _.reduce( - tableRows, - function (prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows).map(col.id).min().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows).map(col.id).max().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter((column) => column); - - if (showPercentage) { - const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); - - // column to show percentage for was removed - if (insertAtIndex < 0) return; - - const { cols, rows } = addPercentageCol( - $scope.formattedColumns, - percentageCol, - table.rows, - insertAtIndex - ); - $scope.rows = rows; - $scope.formattedColumns = cols; - } - } - ); - }, - }; -} - -/** - * @param {Object[]} columns - the formatted columns that will be displayed - * @param {String} title - the title of the column to add to - * @param {Object[]} rows - the row data for the columns - * @param {Number} insertAtIndex - the index to insert the percentage column at - * @returns {Object} - cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol(columns, title, rows, insertAtIndex) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - }); - const newRows = rows.map((row) => ({ - [newId]: row[id] / sumTotal, - ...row, - })); - - return { cols: newCols, rows: newRows }; -} - -function insert(arr, index, ...items) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, ...items); - return newArray; -} diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js deleted file mode 100644 index ecb4ade51b36c..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js +++ /dev/null @@ -1,488 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import $ from 'jquery'; -import moment from 'moment-timezone'; -import angular from 'angular'; -import 'angular-mocks'; -import sinon from 'sinon'; -import { round } from 'lodash'; - -import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; -import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; -import { setUiSettings } from '../../../../data/public/services'; -import { FORMATS_UI_SETTINGS } from '../../../../field_formats/common/'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; - -import { setFormatService } from '../../services'; -import { getInnerAngular } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -const uiSettings = new Map(); - -describe('Table Vis - AggTable Directive', function () { - const core = coreMock.createStart(); - - core.uiSettings.set = jest.fn((key, value) => { - uiSettings.set(key, value); - }); - - core.uiSettings.get = jest.fn((key) => { - const defaultValues = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - 'dateFormat:tz': 'UTC', - [FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE]: true, - [FORMATS_UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', - [FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', - [FORMATS_UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', - [FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', - [FORMATS_UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, - [CSV_SEPARATOR_SETTING]: ',', - [CSV_QUOTE_VALUES_SETTING]: true, - }; - - return defaultValues[key] || uiSettings.get(key); - }); - - let $rootScope; - let $compile; - let settings; - - const initLocalAngular = () => { - const tableVisModule = getInnerAngular('kibana/table_vis', core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeEach(() => { - setUiSettings(core.uiSettings); - setFormatService(getFieldFormatsRegistry(core)); - initLocalAngular(); - angular.mock.module('kibana/table_vis'); - angular.mock.inject(($injector, config) => { - settings = config; - - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }); - }); - - let $scope; - beforeEach(function () { - $scope = $rootScope.$new(); - }); - afterEach(function () { - $scope.$destroy(); - }); - - test('renders a simple response properly', function () { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.table = tabifiedData.metricOnly.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(1); - expect($el.find('td').length).toBe(1); - expect($el.find('td').text()).toEqual('1,000'); - }); - - test('renders nothing if the table is empty', function () { - $scope.dimensions = {}; - $scope.table = null; - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(0); - }); - - test('renders a complex response properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - const $el = $(''); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(1); - - const $rows = $el.find('tbody tr'); - expect($rows.length).toBeGreaterThan(0); - - function validBytes(str) { - const num = str.replace(/,/g, ''); - if (num !== '-') { - expect(num).toMatch(/^\d+$/); - } - } - - $rows.each(function () { - // 6 cells in every row - const $cells = $(this).find('td'); - expect($cells.length).toBe(6); - - const txts = $cells.map(function () { - return $(this).text().trim(); - }); - - // two character country code - expect(txts[0]).toMatch(/^(png|jpg|gif|html|css)$/); - validBytes(txts[1]); - - // country - expect(txts[2]).toMatch(/^\w\w$/); - validBytes(txts[3]); - - // os - expect(txts[4]).toMatch(/^(win|mac|linux)$/); - validBytes(txts[5]); - }); - }); - - describe('renders totals row', function () { - async function totalsRowTest(totalFunc, expected) { - function setDefaultTimezone() { - moment.tz.setDefault(settings.get('dateFormat:tz')); - } - - const oldTimezoneSetting = settings.get('dateFormat:tz'); - settings.set('dateFormat:tz', 'UTC'); - setDefaultTimezone(); - - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.showTotal = true; - $scope.totalFunc = totalFunc; - const $el = $(``); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tfoot').length).toBe(1); - - const $rows = $el.find('tfoot tr'); - expect($rows.length).toBe(1); - - const $cells = $($rows[0]).find('th'); - expect($cells.length).toBe(6); - - for (let i = 0; i < 6; i++) { - expect($($cells[i]).text().trim()).toBe(expected[i]); - } - settings.set('dateFormat:tz', oldTimezoneSetting); - setDefaultTimezone(); - } - test('as count', async function () { - await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); - }); - test('as min', async function () { - await totalsRowTest('min', [ - '', - '2014-09-28', - '9,283', - 'Sep 28, 2014 @ 00:00:00.000', - '1', - '11', - ]); - }); - test('as max', async function () { - await totalsRowTest('max', [ - '', - '2014-10-03', - '220,943', - 'Oct 3, 2014 @ 00:00:00.000', - '239', - '837', - ]); - }); - test('as avg', async function () { - await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); - }); - test('as sum', async function () { - await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); - }); - }); - - describe('aggTable.toCsv()', function () { - test('escapes rows and columns properly', function () { - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.rows = [{ a: 1, b: 2, c: '"foobar"' }]; - $tableScope.formattedColumns = [ - { id: 'a', title: 'one' }, - { id: 'b', title: 'two' }, - { id: 'c', title: 'with double-quotes(")' }, - ]; - - expect(aggTable.toCsv()).toBe( - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' - ); - }); - - test('exports rows and columns properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - const raw = aggTable.toCsv(false); - expect(raw).toBe( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - 'png,412032,IT,9299,win,0' + - '\r\n' + - 'png,412032,IT,9299,mac,9299' + - '\r\n' + - 'png,412032,US,8293,linux,3992' + - '\r\n' + - 'png,412032,US,8293,mac,3029' + - '\r\n' + - 'css,412032,MX,9299,win,4992' + - '\r\n' + - 'css,412032,MX,9299,mac,5892' + - '\r\n' + - 'css,412032,US,8293,linux,3992' + - '\r\n' + - 'css,412032,US,8293,mac,3029' + - '\r\n' + - 'html,412032,CN,9299,win,4992' + - '\r\n' + - 'html,412032,CN,9299,mac,5892' + - '\r\n' + - 'html,412032,FR,8293,win,3992' + - '\r\n' + - 'html,412032,FR,8293,mac,3029' + - '\r\n' - ); - }); - - test('exports formatted rows and columns properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - // Create our own converter since the ones we use for tests don't actually transform the provided value - $tableScope.formattedColumns[0].formatter.convert = (v) => `${v}_formatted`; - - const formatted = aggTable.toCsv(true); - expect(formatted).toBe( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - '"png_formatted",412032,IT,9299,win,0' + - '\r\n' + - '"png_formatted",412032,IT,9299,mac,9299' + - '\r\n' + - '"png_formatted",412032,US,8293,linux,3992' + - '\r\n' + - '"png_formatted",412032,US,8293,mac,3029' + - '\r\n' + - '"css_formatted",412032,MX,9299,win,4992' + - '\r\n' + - '"css_formatted",412032,MX,9299,mac,5892' + - '\r\n' + - '"css_formatted",412032,US,8293,linux,3992' + - '\r\n' + - '"css_formatted",412032,US,8293,mac,3029' + - '\r\n' + - '"html_formatted",412032,CN,9299,win,4992' + - '\r\n' + - '"html_formatted",412032,CN,9299,mac,5892' + - '\r\n' + - '"html_formatted",412032,FR,8293,win,3992' + - '\r\n' + - '"html_formatted",412032,FR,8293,mac,3029' + - '\r\n' - ); - }); - }); - - test('renders percentage columns', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.percentageCol = 'Average bytes'; - - const $el = $(``); - - $compile($el)($scope); - $scope.$digest(); - - const $headings = $el.find('th'); - expect($headings.length).toBe(7); - expect($headings.eq(3).text().trim()).toBe('Average bytes percentages'); - - const countColId = $scope.table.columns.find((col) => col.name === $scope.percentageCol).id; - const counts = $scope.table.rows.map((row) => row[countColId]); - const total = counts.reduce((sum, curr) => sum + curr, 0); - const $percentageColValues = $el.find('tbody tr').map((i, el) => $(el).find('td').eq(3).text()); - - $percentageColValues.each((i, value) => { - const percentage = `${round((counts[i] / total) * 100, 3)}%`; - expect(value).toBe(percentage); - }); - }); - - describe('aggTable.exportAsCsv()', function () { - let origBlob; - function FakeBlob(slices, opts) { - this.slices = slices; - this.opts = opts; - } - - beforeEach(function () { - origBlob = window.Blob; - window.Blob = FakeBlob; - }); - - afterEach(function () { - window.Blob = origBlob; - }); - - test('calls _saveAs properly', function () { - const $el = $compile('')($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - - const saveAs = sinon.stub(aggTable, '_saveAs'); - $tableScope.rows = [{ a: 1, b: 2, c: '"foobar"' }]; - $tableScope.formattedColumns = [ - { id: 'a', title: 'one' }, - { id: 'b', title: 'two' }, - { id: 'c', title: 'with double-quotes(")' }, - ]; - - aggTable.csv.filename = 'somefilename.csv'; - aggTable.exportAsCsv(); - - expect(saveAs.callCount).toBe(1); - const call = saveAs.getCall(0); - expect(call.args[0]).toBeInstanceOf(FakeBlob); - expect(call.args[0].slices).toEqual([ - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', - ]); - expect(call.args[0].opts).toEqual({ - type: 'text/plain;charset=utf-8', - }); - expect(call.args[1]).toBe('somefilename.csv'); - }); - - test('should use the export-title attribute', function () { - const expected = 'export file name'; - const $el = $compile( - `` - )($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [], - rows: [], - }; - $tableScope.exportTitle = expected; - $scope.$digest(); - - expect(aggTable.csv.filename).toEqual(`${expected}.csv`); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html deleted file mode 100644 index 4567b80b5f66c..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - -
- {{ table.title }} -
- - - -
- - - - - - - - - - - - -
- {{ table.title }} -
- - - -
diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js deleted file mode 100644 index e9e71806a2bc3..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.js +++ /dev/null @@ -1,45 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import aggTableGroupTemplate from './agg_table_group.html'; - -export function KbnAggTableGroup(RecursionHelper) { - return { - restrict: 'E', - template: aggTableGroupTemplate, - scope: { - group: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el, { - post: function ($scope) { - $scope.$watch('group', function (group) { - // clear the previous "state" - $scope.rows = $scope.columns = false; - - if (!group || !group.tables.length) return; - - const childLayout = group.direction === 'row' ? 'rows' : 'columns'; - - $scope[childLayout] = group.tables; - }); - }, - }); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js deleted file mode 100644 index ba04b2f449f6d..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js +++ /dev/null @@ -1,132 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import $ from 'jquery'; -import angular from 'angular'; -import 'angular-mocks'; -import expect from '@kbn/expect'; - -import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; -import { coreMock } from '../../../../../core/public/mocks'; -import { setUiSettings } from '../../../../data/public/services'; -import { setFormatService } from '../../services'; -import { getInnerAngular } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; -import { tabifiedData } from './tabified_data'; - -const uiSettings = new Map(); - -describe('Table Vis - AggTableGroup Directive', function () { - const core = coreMock.createStart(); - let $rootScope; - let $compile; - - core.uiSettings.set = jest.fn((key, value) => { - uiSettings.set(key, value); - }); - - core.uiSettings.get = jest.fn((key) => { - return uiSettings.get(key); - }); - - const initLocalAngular = () => { - const tableVisModule = getInnerAngular('kibana/table_vis', core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeEach(() => { - setUiSettings(core.uiSettings); - setFormatService(getFieldFormatsRegistry(core)); - initLocalAngular(); - angular.mock.module('kibana/table_vis'); - angular.mock.inject(($injector) => { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }); - }); - - let $scope; - beforeEach(function () { - $scope = $rootScope.$new(); - }); - afterEach(function () { - $scope.$destroy(); - }); - - it('renders a simple split response properly', function () { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.group = tabifiedData.metricOnly; - $scope.sort = { - columnIndex: null, - direction: null, - }; - const $el = $( - '' - ); - - $compile($el)($scope); - $scope.$digest(); - - // should create one sub-tbale - expect($el.find('kbn-agg-table').length).to.be(1); - }); - - it('renders nothing if the table list is empty', function () { - const $el = $( - '' - ); - - $scope.group = { - tables: [], - }; - - $compile($el)($scope); - $scope.$digest(); - - const $subTables = $el.find('kbn-agg-table'); - expect($subTables.length).to.be(0); - }); - - it('renders a complex response properly', function () { - $scope.dimensions = { - splitRow: [{ accessor: 0, params: {} }], - buckets: [ - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - const group = ($scope.group = tabifiedData.threeTermBucketsWithSplit); - const $el = $( - '' - ); - $compile($el)($scope); - $scope.$digest(); - - const $subTables = $el.find('kbn-agg-table'); - expect($subTables.length).to.be(3); - - const $subTableHeaders = $el.find('.kbnAggTable__groupHeader'); - expect($subTableHeaders.length).to.be(3); - - $subTableHeaders.each(function (i) { - expect($(this).text()).to.be(group.tables[i].title); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js b/src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js deleted file mode 100644 index 54da06f2a5f90..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/agg_table/tabified_data.js +++ /dev/null @@ -1,784 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const tabifiedData = { - metricOnly: { - tables: [ - { - columns: [ - { - id: 'col-0-1', - name: 'Count', - }, - ], - rows: [ - { - 'col-0-1': 1000, - }, - ], - }, - ], - }, - threeTermBuckets: { - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_1', - name: 'Average bytes', - }, - { - id: 'col-2-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - { - id: 'col-4-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-5-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'IT', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'IT', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'linux', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'MX', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'MX', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'linux', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'CN', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'CN', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'FR', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'FR', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - ], - }, - ], - }, - threeTermBucketsWithSplit: { - tables: [ - { - title: 'png: extension: Descending', - name: 'extension: Descending', - key: 'png', - column: 0, - row: 0, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - { - title: 'css: extension: Descending', - name: 'extension: Descending', - key: 'css', - column: 0, - row: 4, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - { - title: 'html: extension: Descending', - name: 'extension: Descending', - key: 'html', - column: 0, - row: 8, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - ], - direction: 'row', - }, - oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative: { - tables: [ - { - columns: [ - { - id: 'col-0-agg_3', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_4', - name: '@timestamp per day', - }, - { - id: 'col-2-agg_1', - name: 'Average bytes', - }, - { - id: 'col-3-agg_2', - name: 'Min @timestamp', - }, - { - id: 'col-4-agg_5', - name: 'Derivative of Count', - }, - { - id: 'col-5-agg_6', - name: 'Last bytes', - }, - ], - rows: [ - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 23, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 203, - 'col-5-agg_6': 39, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 200, - 'col-5-agg_6': 329, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 103, - 'col-5-agg_6': 22, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 153, - 'col-5-agg_6': 93, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 239, - 'col-5-agg_6': 72, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 75, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 10, - 'col-5-agg_6': 11, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 24, - 'col-5-agg_6': 238, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 49, - 'col-5-agg_6': 343, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 100, - 'col-5-agg_6': 837, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 23, - 'col-5-agg_6': 302, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 30, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 1, - 'col-5-agg_6': 43, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 5, - 'col-5-agg_6': 88, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 10, - 'col-5-agg_6': 91, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 43, - 'col-5-agg_6': 534, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 1, - 'col-5-agg_6': 553, - }, - ], - }, - ], - }, -}; diff --git a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts deleted file mode 100644 index 412dd904a5e87..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts +++ /dev/null @@ -1,88 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// inner angular imports -// these are necessary to bootstrap the local angular. -// They can stay even after NP cutover -import angular from 'angular'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; -import 'angular-recursion'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; -import { - PaginateDirectiveProvider, - PaginateControlsDirectiveProvider, - PrivateProvider, - watchMultiDecorator, - KbnAccessibleClickProvider, -} from '../../../kibana_legacy/public'; - -const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; - -export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { - const uiModule = getInnerAngular(name, core); - return uiModule; -} - -let initialized = false; - -export function getInnerAngular(name = 'kibana/table_vis', core: CoreStart) { - if (!initialized) { - createLocalPrivateModule(); - createLocalI18nModule(); - createLocalConfigModule(core.uiSettings); - createLocalPaginateModule(); - initialized = true; - } - return angular - .module(name, [ - ...thirdPartyAngularDependencies, - 'tableVisPaginate', - 'tableVisConfig', - 'tableVisPrivate', - 'tableVisI18n', - ]) - .config(watchMultiDecorator) - .directive('kbnAccessibleClick', KbnAccessibleClickProvider); -} - -function createLocalPrivateModule() { - angular.module('tableVisPrivate', []).provider('Private', PrivateProvider); -} - -function createLocalConfigModule(uiSettings: IUiSettingsClient) { - angular.module('tableVisConfig', []).provider('config', function () { - return { - $get: () => ({ - get: (value: string) => { - return uiSettings ? uiSettings.get(value) : undefined; - }, - // set method is used in agg_table mocha test - set: (key: string, value: string) => { - return uiSettings ? uiSettings.set(key, value) : undefined; - }, - }), - }; - }); -} - -function createLocalI18nModule() { - angular - .module('tableVisI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createLocalPaginateModule() { - angular - .module('tableVisPaginate', []) - .directive('paginate', PaginateDirectiveProvider) - .directive('paginateControls', PaginateControlsDirectiveProvider); -} diff --git a/src/plugins/vis_type_table/public/legacy/index.scss b/src/plugins/vis_type_table/public/legacy/index.scss deleted file mode 100644 index 0972c85e0dbe0..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Prefix all styles with "tbv" to avoid conflicts. -// Examples -// tbvChart -// tbvChart__legend -// tbvChart__legend--small -// tbvChart__legend-isLoading - -@import './agg_table/index'; -@import './paginated_table/index'; -@import './table_vis'; diff --git a/src/plugins/vis_type_table/public/legacy/index.ts b/src/plugins/vis_type_table/public/legacy/index.ts deleted file mode 100644 index 6f7eb0686852f..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { registerLegacyVis } from './register_legacy_vis'; diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss b/src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss deleted file mode 100644 index 23d56c09b2818..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './_table_cell_filter'; diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss deleted file mode 100644 index 05d050362ce0b..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/_table_cell_filter.scss +++ /dev/null @@ -1,30 +0,0 @@ -.kbnTableCellFilter__hover { - position: relative; - - /** - * 1. Center vertically regardless of row height. - */ - .kbnTableCellFilter { - position: absolute; - white-space: nowrap; - right: 0; - top: 50%; /* 1 */ - transform: translateY(-50%); /* 1 */ - display: none; - } - - &:hover { - .kbnTableCellFilter { - display: inline; - } - - .kbnTableCellFilter__hover-show { - visibility: visible; - } - } -} - -.kbnTableCellFilter__hover-show { - // so that the cell doesn't change size on hover - visibility: hidden; -} diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html deleted file mode 100644 index 12731eb386566..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.html +++ /dev/null @@ -1,55 +0,0 @@ - -
- - - - - - - - - - - - - -
- - - - - - -
- {{ col.formattedTotal }} -
-
- - - -
-
diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js deleted file mode 100644 index 066134dbb5dc5..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.js +++ /dev/null @@ -1,98 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import paginatedTableTemplate from './paginated_table.html'; - -export function PaginatedTable($filter) { - const orderBy = $filter('orderBy'); - - return { - restrict: 'E', - template: paginatedTableTemplate, - transclude: true, - scope: { - table: '=', - rows: '=', - columns: '=', - linkToTop: '=', - perPage: '=?', - sortHandler: '=?', - sort: '=?', - showSelector: '=?', - showTotal: '=', - totalFunc: '=', - filter: '=', - percentageCol: '=', - }, - controllerAs: 'paginatedTable', - controller: function ($scope) { - const self = this; - self.sort = { - columnIndex: null, - direction: null, - }; - - self.sortColumn = function (colIndex, sortDirection = 'asc') { - const col = $scope.columns[colIndex]; - - if (!col) return; - if (col.sortable === false) return; - - if (self.sort.columnIndex === colIndex) { - const directions = { - null: 'asc', - asc: 'desc', - desc: null, - }; - sortDirection = directions[self.sort.direction]; - } - - self.sort.columnIndex = colIndex; - self.sort.direction = sortDirection; - if ($scope.sort) { - _.assign($scope.sort, self.sort); - } - }; - - function valueGetter(row) { - const col = $scope.columns[self.sort.columnIndex]; - let value = row[col.id]; - if (typeof value === 'boolean') value = value ? 0 : 1; - return value; - } - - // Set the sort state if it is set - if ($scope.sort && $scope.sort.columnIndex !== null) { - self.sortColumn($scope.sort.columnIndex, $scope.sort.direction); - } - - function resortRows() { - const newSort = $scope.sort; - if (newSort && !_.isEqual(newSort, self.sort)) { - self.sortColumn(newSort.columnIndex, newSort.direction); - } - - if (!$scope.rows || !$scope.columns) { - $scope.sortedRows = false; - return; - } - - const sort = self.sort; - if (sort.direction == null) { - $scope.sortedRows = $scope.rows.slice(0); - } else { - $scope.sortedRows = orderBy($scope.rows, valueGetter, sort.direction === 'desc'); - } - } - - // update the sortedRows result - $scope.$watchMulti(['rows', 'columns', '[]sort', '[]paginatedTable.sort'], resortRows); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts deleted file mode 100644 index 3feff52f86792..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts +++ /dev/null @@ -1,464 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { isNumber, times, identity, random } from 'lodash'; -import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; -import 'angular-sanitize'; -import 'angular-mocks'; - -import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; -import { getAngularModule } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../../core/public/mocks'; - -interface Sort { - columnIndex: number; - direction: string; -} - -interface Row { - [key: string]: number | string; -} - -interface Column { - id?: string; - title: string; - formatter?: { - convert?: (val: string) => string; - }; - sortable?: boolean; -} - -interface Table { - columns: Column[]; - rows: Row[]; -} - -interface PaginatedTableScope extends IScope { - table?: Table; - cols?: Column[]; - rows?: Row[]; - perPage?: number; - sort?: Sort; - linkToTop?: boolean; -} - -describe('Table Vis - Paginated table', () => { - let $el: JQuery; - let $rootScope: IRootScopeService; - let $compile: ICompileService; - let $scope: PaginatedTableScope; - const defaultPerPage = 10; - let paginatedTable: any; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - - const initLocalAngular = () => { - const tableVisModule = getAngularModule( - 'kibana/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('kibana/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - $scope = $rootScope.$new(); - }) - ); - - afterEach(() => { - $scope.$destroy(); - }); - - const makeData = (colCount: number | Column[], rowCount: number | string[][]) => { - let columns: Column[] = []; - let rows: Row[] = []; - - if (isNumber(colCount)) { - times(colCount, (i) => { - columns.push({ id: `${i}`, title: `column${i}`, formatter: { convert: identity } }); - }); - } else { - columns = colCount.map( - (col, i) => - ({ - id: `${i}`, - title: col.title, - formatter: col.formatter || { convert: identity }, - } as Column) - ); - } - - if (isNumber(rowCount)) { - times(rowCount, (row) => { - const rowItems: Row = {}; - - times(columns.length, (col) => { - rowItems[`${col}`] = `item-${col}-${row}`; - }); - - rows.push(rowItems); - }); - } else { - rows = rowCount.map((row: string[]) => { - const newRow: Row = {}; - row.forEach((v, i) => (newRow[i] = v)); - return newRow; - }); - } - - return { - columns, - rows, - }; - }; - - const renderTable = ( - table: { columns: Column[]; rows: Row[] } | null, - cols: Column[], - rows: Row[], - perPage?: number, - sort?: Sort, - linkToTop?: boolean - ) => { - $scope.table = table || { columns: [], rows: [] }; - $scope.cols = cols || []; - $scope.rows = rows || []; - $scope.perPage = perPage || defaultPerPage; - $scope.sort = sort; - $scope.linkToTop = linkToTop; - - const template = ` - `; - const element = $compile(template)($scope); - $el = $(element); - - $scope.$digest(); - paginatedTable = element.controller('paginatedTable'); - }; - - describe('rendering', () => { - test('should not display without rows', () => { - const cols: Column[] = [ - { - id: 'col-1-1', - title: 'test1', - }, - ]; - const rows: Row[] = []; - - renderTable(null, cols, rows); - expect($el.children().length).toBe(0); - }); - - test('should render columns and rows', () => { - const data = makeData(2, 2); - const cols = data.columns; - const rows = data.rows; - - renderTable(data, cols, rows); - expect($el.children().length).toBe(1); - const tableRows = $el.find('tbody tr'); - - // should contain the row data - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(rows[0][0]); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe(rows[0][1]); - expect(tableRows.eq(1).find('td').eq(0).text()).toBe(rows[1][0]); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe(rows[1][1]); - }); - - test('should paginate rows', () => { - // note: paginate truncates pages, so don't make too many - const rowCount = random(16, 24); - const perPageCount = random(5, 8); - const data = makeData(3, rowCount); - const pageCount = Math.ceil(rowCount / perPageCount); - - renderTable(data, data.columns, data.rows, perPageCount); - const tableRows = $el.find('tbody tr'); - expect(tableRows.length).toBe(perPageCount); - // add 2 for the first and last page links - expect($el.find('paginate-controls button').length).toBe(pageCount + 2); - }); - - test('should not show blank rows on last page', () => { - const rowCount = 7; - const perPageCount = 10; - const data = makeData(3, rowCount); - - renderTable(data, data.columns, data.rows, perPageCount); - const tableRows = $el.find('tbody tr'); - expect(tableRows.length).toBe(rowCount); - }); - - test('should not show link to top when not set', () => { - const data = makeData(5, 5); - renderTable(data, data.columns, data.rows, 10); - - const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]'); - expect(linkToTop.length).toBe(0); - }); - - test('should show link to top when set', () => { - const data = makeData(5, 5); - renderTable(data, data.columns, data.rows, 10, undefined, true); - - const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]'); - expect(linkToTop.length).toBe(1); - }); - }); - - describe('sorting', () => { - let data: Table; - let lastRowIndex: number; - - beforeEach(() => { - data = makeData(3, [ - ['bbbb', 'aaaa', 'zzzz'], - ['cccc', 'cccc', 'aaaa'], - ['zzzz', 'bbbb', 'bbbb'], - ['aaaa', 'zzzz', 'cccc'], - ]); - - lastRowIndex = data.rows.length - 1; - renderTable(data, data.columns, data.rows); - }); - - test('should not sort by default', () => { - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(data.rows[0][0]); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe(data.rows[lastRowIndex][0]); - }); - - test('should do nothing when sorting by invalid column id', () => { - // sortColumn - paginatedTable.sortColumn(999); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('zzzz'); - }); - - test('should do nothing when sorting by non sortable column', () => { - data.columns[0].sortable = false; - - // sortColumn - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('zzzz'); - }); - - test("should set the sort direction to asc when it's not explicitly set", () => { - paginatedTable.sortColumn(1); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(2).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - }); - - test('should allow you to explicitly set the sort direction', () => { - paginatedTable.sortColumn(1, 'desc'); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('zzzz'); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(2).find('td').eq(1).text()).toBe('bbbb'); - }); - - test('should sort ascending on first invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('aaaa'); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('zzzz'); - }); - - test('should sort descending on second invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('zzzz'); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('aaaa'); - }); - - test('should clear sorting on third invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(data.rows[0][0]); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('aaaa'); - }); - - test('should sort new column ascending', () => { - // sort by first column - paginatedTable.sortColumn(0); - $scope.$digest(); - - // sort by second column - paginatedTable.sortColumn(1); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(lastRowIndex).find('td').eq(1).text()).toBe('zzzz'); - }); - }); - - describe('sorting duplicate columns', () => { - let data; - const colText = 'test row'; - - beforeEach(() => { - const cols: Column[] = [{ title: colText }, { title: colText }, { title: colText }]; - const rows = [ - ['bbbb', 'aaaa', 'zzzz'], - ['cccc', 'cccc', 'aaaa'], - ['zzzz', 'bbbb', 'bbbb'], - ['aaaa', 'zzzz', 'cccc'], - ]; - data = makeData(cols, rows); - - renderTable(data, data.columns, data.rows); - }); - - test('should have duplicate column titles', () => { - const columns = $el.find('thead th span'); - columns.each((i, col) => { - expect($(col).text()).toBe(colText); - }); - }); - - test('should handle sorting on columns with the same name', () => { - // sort by the last column - paginatedTable.sortColumn(2); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('aaaa'); - expect(tableRows.eq(1).find('td').eq(2).text()).toBe('bbbb'); - expect(tableRows.eq(2).find('td').eq(2).text()).toBe('cccc'); - expect(tableRows.eq(3).find('td').eq(2).text()).toBe('zzzz'); - }); - - test('should sort correctly between columns', () => { - // sort by the last column - paginatedTable.sortColumn(2); - $scope.$digest(); - - let tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('aaaa'); - - // sort by the first column - paginatedTable.sortColumn(0); - $scope.$digest(); - - tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('zzzz'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('cccc'); - - expect(tableRows.eq(1).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(2).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(3).find('td').eq(0).text()).toBe('zzzz'); - }); - - test('should not sort duplicate columns', () => { - paginatedTable.sortColumn(1); - $scope.$digest(); - - const sorters = $el.find('thead th i'); - expect(sorters.eq(0).hasClass('fa-sort')).toBe(true); - expect(sorters.eq(1).hasClass('fa-sort')).toBe(false); - expect(sorters.eq(2).hasClass('fa-sort')).toBe(true); - }); - }); - - describe('object rows', () => { - let cols: Column[]; - let rows: any; - - beforeEach(() => { - cols = [ - { - title: 'object test', - id: '0', - formatter: { - convert: (val) => { - return val === 'zzz' ? '

hello

' : val; - }, - }, - }, - ]; - rows = [['aaaa'], ['zzz'], ['bbbb']]; - renderTable({ columns: cols, rows }, cols, rows); - }); - - test('should append object markup', () => { - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('h1').length).toBe(0); - expect(tableRows.eq(1).find('h1').length).toBe(1); - expect(tableRows.eq(2).find('h1').length).toBe(0); - }); - - test('should sort using object value', () => { - paginatedTable.sortColumn(0); - $scope.$digest(); - let tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('h1').length).toBe(0); - expect(tableRows.eq(1).find('h1').length).toBe(0); - // html row should be the last row - expect(tableRows.eq(2).find('h1').length).toBe(1); - - paginatedTable.sortColumn(0); - $scope.$digest(); - tableRows = $el.find('tbody tr'); - // html row should be the first row - expect(tableRows.eq(0).find('h1').length).toBe(1); - expect(tableRows.eq(1).find('h1').length).toBe(0); - expect(tableRows.eq(2).find('h1').length).toBe(0); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js b/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js deleted file mode 100644 index d06e9e3bd870e..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/rows.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import angular from 'angular'; -import tableCellFilterHtml from './table_cell_filter.html'; - -export function KbnRows($compile) { - return { - restrict: 'A', - link: function ($scope, $el, attr) { - function addCell($tr, contents, column, row) { - function createCell() { - return $(document.createElement('td')); - } - - function createFilterableCell(value) { - const $template = $(tableCellFilterHtml); - $template.addClass('kbnTableCellFilter__hover'); - - const scope = $scope.$new(); - - scope.onFilterClick = (event, negate) => { - // Don't add filter if a link was clicked. - if ($(event.target).is('a')) { - return; - } - - $scope.filter({ - name: 'filterBucket', - data: { - data: [ - { - table: $scope.table, - row: $scope.rows.findIndex((r) => r === row), - column: $scope.table.columns.findIndex((c) => c.id === column.id), - value, - }, - ], - negate, - }, - }); - }; - - return $compile($template)(scope); - } - - let $cell; - let $cellContent; - - const contentsIsDefined = contents !== null && contents !== undefined; - - if (column.filterable && contentsIsDefined) { - $cell = createFilterableCell(contents); - // in jest tests 'angular' is using jqLite. In jqLite the method find lookups only by tags. - // Because of this, we should change a way how we get cell content so that tests will pass. - $cellContent = angular.element($cell[0].querySelector('[data-cell-content]')); - } else { - $cell = $cellContent = createCell(); - } - - // An AggConfigResult can "enrich" cell contents by applying a field formatter, - // which we want to do if possible. - contents = contentsIsDefined ? column.formatter.convert(contents, 'html') : ''; - - if (_.isObject(contents)) { - if (contents.attr) { - $cellContent.attr(contents.attr); - } - - if (contents.class) { - $cellContent.addClass(contents.class); - } - - if (contents.scope) { - $cellContent = $compile($cellContent.prepend(contents.markup))(contents.scope); - } else { - $cellContent.prepend(contents.markup); - } - - if (contents.attr) { - $cellContent.attr(contents.attr); - } - } else { - if (contents === '') { - $cellContent.prepend(' '); - } else { - $cellContent.prepend(contents); - } - } - - $tr.append($cell); - } - - $scope.$watchMulti([attr.kbnRows, attr.kbnRowsMin], function (vals) { - let rows = vals[0]; - const min = vals[1]; - - $el.empty(); - - if (!Array.isArray(rows)) rows = []; - - if (isFinite(min) && rows.length < min) { - // clone the rows so that we can add elements to it without upsetting the original - rows = _.clone(rows); - // crate the empty row which will be pushed into the row list over and over - const emptyRow = {}; - // push as many empty rows into the row array as needed - _.times(min - rows.length, function () { - rows.push(emptyRow); - }); - } - - rows.forEach(function (row) { - const $tr = $(document.createElement('tr')).appendTo($el); - $scope.columns.forEach((column) => { - const value = row[column.id]; - addCell($tr, value, column, row); - }); - }); - }); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html deleted file mode 100644 index 57ecb9b221611..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/table_cell_filter.html +++ /dev/null @@ -1,23 +0,0 @@ - -
- - - - - -
- diff --git a/src/plugins/vis_type_table/public/legacy/register_legacy_vis.ts b/src/plugins/vis_type_table/public/legacy/register_legacy_vis.ts deleted file mode 100644 index 447140267a3db..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/register_legacy_vis.ts +++ /dev/null @@ -1,26 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { METRIC_TYPE } from '@kbn/analytics'; -import { PluginInitializerContext, CoreSetup } from 'kibana/public'; - -import { TablePluginSetupDependencies, TablePluginStartDependencies } from '../plugin'; -import { createTableVisLegacyFn } from './table_vis_legacy_fn'; -import { getTableVisLegacyRenderer } from './table_vis_legacy_renderer'; -import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type'; - -export const registerLegacyVis = ( - core: CoreSetup, - { expressions, visualizations, usageCollection }: TablePluginSetupDependencies, - context: PluginInitializerContext -) => { - usageCollection?.reportUiCounter('vis_type_table', METRIC_TYPE.LOADED, 'legacyVisEnabled'); - expressions.registerFunction(createTableVisLegacyFn); - expressions.registerRenderer(getTableVisLegacyRenderer(core, context)); - visualizations.createBaseVisualization(tableVisLegacyTypeDefinition); -}; diff --git a/src/plugins/vis_type_table/public/legacy/table_vis.html b/src/plugins/vis_type_table/public/legacy/table_vis.html deleted file mode 100644 index c469cd250755c..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
- - -
-
- -

-

-
-
- -
- - -
-
diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_controller.js b/src/plugins/vis_type_table/public/legacy/table_vis_controller.js deleted file mode 100644 index 038c0947d3ed6..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_controller.js +++ /dev/null @@ -1,45 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { assign } from 'lodash'; - -export function TableVisController($scope) { - const uiStateSort = $scope.uiState ? $scope.uiState.get('vis.params.sort') : {}; - assign($scope.visParams.sort, uiStateSort); - - $scope.sort = $scope.visParams.sort; - $scope.$watchCollection('sort', function (newSort) { - $scope.uiState.set('vis.params.sort', newSort); - }); - - /** - * Recreate the entire table when: - * - the underlying data changes (esResponse) - * - one of the view options changes (vis.params) - */ - $scope.$watch('renderComplete', function () { - let tableGroups = ($scope.tableGroups = null); - let hasSomeRows = ($scope.hasSomeRows = null); - - if ($scope.esResponse) { - tableGroups = $scope.esResponse; - - hasSomeRows = tableGroups.tables.some(function haveRows(table) { - if (table.tables) return table.tables.some(haveRows); - return table.rows.length > 0; - }); - } - - $scope.hasSomeRows = hasSomeRows; - if (hasSomeRows) { - $scope.dimensions = $scope.visParams.dimensions; - $scope.tableGroups = tableGroups; - } - $scope.renderComplete(); - }); -} diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts deleted file mode 100644 index e53d4e879bb3b..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts +++ /dev/null @@ -1,245 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular, { ICompileService, IRootScopeService, IScope } from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import $ from 'jquery'; - -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { initAngularBootstrap } from '../../../kibana_legacy/public/angular_bootstrap'; -import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type'; -import { Vis } from '../../../visualizations/public'; -import { createStubIndexPattern, stubFieldSpecMap } from '../../../data/public/stubs'; -import { tableVisLegacyResponseHandler } from './table_vis_legacy_response_handler'; -import { coreMock } from '../../../../core/public/mocks'; -import { IAggConfig, IndexPattern, search } from '../../../data/public'; -import { searchServiceMock } from '../../../data/public/search/mocks'; - -const { createAggConfigs } = searchServiceMock.createStartContract().aggs; - -const { tabifyAggResponse } = search; - -interface TableVisScope extends IScope { - [key: string]: any; -} - -const oneRangeBucket = { - hits: { - total: 6039, - max_score: 0, - hits: [], - }, - aggregations: { - agg_2: { - buckets: { - '0.0-1000.0': { - from: 0, - from_as_string: '0.0', - to: 1000, - to_as_string: '1000.0', - doc_count: 606, - }, - '1000.0-2000.0': { - from: 1000, - from_as_string: '1000.0', - to: 2000, - to_as_string: '2000.0', - doc_count: 298, - }, - }, - }, - }, -}; - -describe('Table Vis - Controller', () => { - let $rootScope: IRootScopeService & { [key: string]: any }; - let $compile: ICompileService; - let $scope: TableVisScope; - let $el: JQuery; - let tableAggResponse: any; - let tabifiedResponse: any; - let stubIndexPattern: IndexPattern; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule( - 'kibana/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('kibana/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - tableAggResponse = tableVisLegacyResponseHandler; - }) - ); - - beforeEach(() => { - stubIndexPattern = createStubIndexPattern({ - spec: { - id: 'logstash-*', - title: 'logstash-*', - timeFieldName: 'time', - fields: stubFieldSpecMap, - }, - }); - }); - - function getRangeVis(params?: object) { - return ({ - type: tableVisLegacyTypeDefinition, - params: Object.assign({}, tableVisLegacyTypeDefinition.visConfig?.defaults, params), - data: { - aggs: createAggConfigs(stubIndexPattern, [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 }, - ], - }, - }, - ]), - }, - } as unknown) as Vis; - } - - const dimensions = { - buckets: [ - { - accessor: 0, - }, - ], - metrics: [ - { - accessor: 1, - format: { id: 'range' }, - }, - ], - }; - - // basically a parameterized beforeEach - function initController(vis: Vis) { - vis.data.aggs!.aggs.forEach((agg: IAggConfig, i: number) => { - agg.id = 'agg_' + (i + 1); - }); - - tabifiedResponse = tabifyAggResponse(vis.data.aggs!, oneRangeBucket); - $rootScope.vis = vis; - $rootScope.visParams = vis.params; - $rootScope.uiState = { - get: jest.fn(), - set: jest.fn(), - }; - $rootScope.renderComplete = () => {}; - $rootScope.newScope = (scope: TableVisScope) => { - $scope = scope; - }; - - $el = $('
') - .attr('ng-controller', 'KbnTableVisController') - .attr('ng-init', 'newScope(this)'); - - $compile($el)($rootScope); - } - - // put a response into the controller - function attachEsResponseToScope(resp: object) { - $rootScope.esResponse = resp; - $rootScope.$apply(); - } - - // remove the response from the controller - function removeEsResponseFromScope() { - delete $rootScope.esResponse; - $rootScope.renderComplete = () => {}; - $rootScope.$apply(); - } - - test('exposes #tableGroups and #hasSomeRows when a response is attached to scope', async () => { - const vis: Vis = getRangeVis(); - initController(vis); - - expect(!$scope.tableGroups).toBeTruthy(); - expect(!$scope.hasSomeRows).toBeTruthy(); - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.hasSomeRows).toBeTruthy(); - expect($scope.tableGroups.tables).toBeDefined(); - expect($scope.tableGroups.tables.length).toBe(1); - expect($scope.tableGroups.tables[0].columns.length).toBe(2); - expect($scope.tableGroups.tables[0].rows.length).toBe(2); - }); - - test('clears #tableGroups and #hasSomeRows when the response is removed', async () => { - const vis = getRangeVis(); - initController(vis); - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - removeEsResponseFromScope(); - - expect(!$scope.hasSomeRows).toBeTruthy(); - expect(!$scope.tableGroups).toBeTruthy(); - }); - - test('sets the sort on the scope when it is passed as a vis param', async () => { - const sortObj = { - columnIndex: 1, - direction: 'asc', - }; - const vis = getRangeVis({ sort: sortObj }); - initController(vis); - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.sort.columnIndex).toEqual(sortObj.columnIndex); - expect($scope.sort.direction).toEqual(sortObj.direction); - }); - - test('sets #hasSomeRows properly if the table group is empty', async () => { - const vis = getRangeVis(); - initController(vis); - - tabifiedResponse.rows = []; - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.hasSomeRows).toBeFalsy(); - expect(!$scope.tableGroups).toBeTruthy(); - }); - - test('passes partialRows:true to tabify based on the vis params', () => { - const vis = getRangeVis({ showPartialRows: true }); - initController(vis); - - expect((vis.type.hierarchicalData as Function)(vis)).toEqual(true); - }); - - test('passes partialRows:false to tabify based on the vis params', () => { - const vis = getRangeVis({ showPartialRows: false }); - initController(vis); - - expect((vis.type.hierarchicalData as Function)(vis)).toEqual(false); - }); -}); diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.test.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.test.ts deleted file mode 100644 index 694edc66914be..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.test.ts +++ /dev/null @@ -1,67 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { createTableVisLegacyFn } from './table_vis_legacy_fn'; -import { tableVisLegacyResponseHandler } from './table_vis_legacy_response_handler'; - -import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; - -jest.mock('./table_vis_legacy_response_handler', () => ({ - tableVisLegacyResponseHandler: jest.fn().mockReturnValue({ - tables: [{ columns: [], rows: [] }], - }), -})); - -describe('interpreter/functions#table', () => { - const fn = functionWrapper(createTableVisLegacyFn()); - const context = { - type: 'datatable', - rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - }; - const visConfig = { - title: 'My Chart title', - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - dimensions: { - metrics: [ - { - accessor: 0, - format: { - id: 'number', - }, - params: {}, - aggType: 'count', - }, - ], - buckets: [], - }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('returns an object with the correct structure', async () => { - const actual = await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined); - expect(actual).toMatchSnapshot(); - }); - - it('calls response handler with correct values', async () => { - await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined); - expect(tableVisLegacyResponseHandler).toHaveBeenCalledTimes(1); - expect(tableVisLegacyResponseHandler).toHaveBeenCalledWith(context, visConfig.dimensions); - }); -}); diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts deleted file mode 100644 index 01f0e45ec9c26..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public'; -import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler'; -import { TableVisConfig } from '../types'; -import { VIS_TYPE_TABLE } from '../../common'; - -export type Input = Datatable; - -interface Arguments { - visConfig: string | null; -} - -export interface TableVisRenderValue { - visData: TableContext; - visType: typeof VIS_TYPE_TABLE; - visConfig: TableVisConfig; -} - -export type TableExpressionFunctionDefinition = ExpressionFunctionDefinition< - 'kibana_table', - Input, - Arguments, - Render ->; - -export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => ({ - name: 'kibana_table', - type: 'render', - inputTypes: ['datatable'], - help: i18n.translate('visTypeTable.function.help', { - defaultMessage: 'Table visualization', - }), - args: { - visConfig: { - types: ['string', 'null'], - default: '"{}"', - help: '', - }, - }, - fn(input, args) { - const visConfig = args.visConfig && JSON.parse(args.visConfig); - const convertedData = tableVisLegacyResponseHandler(input, visConfig.dimensions); - - return { - type: 'render', - as: 'table_vis', - value: { - visData: convertedData, - visType: VIS_TYPE_TABLE, - visConfig, - }, - }; - }, -}); diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts deleted file mode 100644 index 59ee876c04278..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_module.ts +++ /dev/null @@ -1,30 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IModule } from 'angular'; - -// @ts-ignore -import { TableVisController } from './table_vis_controller.js'; -// @ts-ignore -import { KbnAggTable } from './agg_table/agg_table'; -// @ts-ignore -import { KbnAggTableGroup } from './agg_table/agg_table_group'; -// @ts-ignore -import { KbnRows } from './paginated_table/rows'; -// @ts-ignore -import { PaginatedTable } from './paginated_table/paginated_table'; - -/** @internal */ -export const initTableVisLegacyModule = (angularIns: IModule): void => { - angularIns - .controller('KbnTableVisController', TableVisController) - .directive('kbnAggTable', KbnAggTable) - .directive('kbnAggTableGroup', KbnAggTableGroup) - .directive('kbnRows', KbnRows) - .directive('paginatedTable', PaginatedTable); -}; diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx deleted file mode 100644 index bab973209c28c..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_renderer.tsx +++ /dev/null @@ -1,42 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; -import { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { TablePluginStartDependencies } from '../plugin'; -import { TableVisRenderValue } from '../table_vis_fn'; -import { TableVisLegacyController } from './vis_controller'; - -const tableVisRegistry = new Map(); - -export const getTableVisLegacyRenderer: ( - core: CoreSetup, - context: PluginInitializerContext -) => ExpressionRenderDefinition = (core, context) => ({ - name: 'table_vis', - reuseDomNode: true, - render: async (domNode, config, handlers) => { - let registeredController = tableVisRegistry.get(domNode); - - if (!registeredController) { - const { getTableVisualizationControllerClass } = await import('./vis_controller'); - - const Controller = getTableVisualizationControllerClass(core, context); - registeredController = new Controller(domNode); - tableVisRegistry.set(domNode, registeredController); - - handlers.onDestroy(() => { - registeredController?.destroy(); - tableVisRegistry.delete(domNode); - }); - } - - await registeredController.render(config.visData, config.visConfig, handlers); - handlers.done(); - }, -}); diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_response_handler.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_response_handler.ts deleted file mode 100644 index cb40b151eb31a..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_response_handler.ts +++ /dev/null @@ -1,95 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Required } from '@kbn/utility-types'; - -import { SchemaConfig } from 'src/plugins/visualizations/public'; -import { getFormatService } from '../services'; -import { Input } from './table_vis_legacy_fn'; - -interface Dimensions { - buckets: SchemaConfig[]; - metrics: SchemaConfig[]; - splitColumn?: SchemaConfig[]; - splitRow?: SchemaConfig[]; -} - -export interface TableContext { - tables: Array; - direction?: 'row' | 'column'; -} - -export interface TableGroup { - $parent: TableContext; - table: Input; - tables: Table[]; - title: string; - name: string; - key: any; - column: number; - row: number; -} - -export interface Table { - $parent?: TableGroup; - columns: Input['columns']; - rows: Input['rows']; -} - -export function tableVisLegacyResponseHandler(table: Input, dimensions: Dimensions): TableContext { - const converted: TableContext = { - tables: [], - }; - - const split = dimensions.splitColumn || dimensions.splitRow; - - if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; - const splitColumnIndex = split[0].accessor; - const splitColumnFormatter = getFormatService().deserialize(split[0].format); - const splitColumn = table.columns[splitColumnIndex]; - const splitMap: Record = {}; - let splitIndex = 0; - - table.rows.forEach((row, rowIndex) => { - const splitValue = row[splitColumn.id]; - - if (!splitMap.hasOwnProperty(splitValue)) { - splitMap[splitValue] = splitIndex++; - const tableGroup: Required = { - $parent: converted, - title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, - name: splitColumn.name, - key: splitValue, - column: splitColumnIndex, - row: rowIndex, - table, - tables: [], - }; - - tableGroup.tables.push({ - $parent: tableGroup, - columns: table.columns, - rows: [], - }); - - converted.tables.push(tableGroup); - } - - const tableIndex = splitMap[splitValue]; - (converted.tables[tableIndex] as TableGroup).tables[0].rows.push(row); - }); - } else { - converted.tables.push({ - columns: table.columns, - rows: table.rows, - }); - } - - return converted; -} diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts deleted file mode 100644 index e582f098a5fd5..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ /dev/null @@ -1,85 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { AggGroupNames } from '../../../data/public'; -import { VisTypeDefinition } from '../../../visualizations/public'; - -import { TableOptions } from '../components/table_vis_options_lazy'; -import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; -import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; -import { toExpressionAstLegacy } from './to_ast_legacy'; - -export const tableVisLegacyTypeDefinition: VisTypeDefinition = { - name: VIS_TYPE_TABLE, - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display data in rows and columns.', - }), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', - }, - }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: [ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], - aggSettings: { - top_hits: { - allowStrings: true, - }, - }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], - }, - ], - }, - toExpressionAst: toExpressionAstLegacy, - hierarchicalData: (vis) => vis.params.showPartialRows || vis.params.showMetricsAtAllLevels, - requiresSearch: true, -}; diff --git a/src/plugins/vis_type_table/public/legacy/to_ast_legacy.ts b/src/plugins/vis_type_table/public/legacy/to_ast_legacy.ts deleted file mode 100644 index b4c8505bbde76..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/to_ast_legacy.ts +++ /dev/null @@ -1,70 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '../../../data/public'; -import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; -import { getVisSchemas, VisToExpressionAst } from '../../../visualizations/public'; -import { TableVisParams } from '../../common'; -import { TableExpressionFunctionDefinition } from './table_vis_legacy_fn'; - -const buildTableVisConfig = ( - schemas: ReturnType, - visParams: TableVisParams -) => { - const metrics = schemas.metric; - const buckets = schemas.bucket || []; - const visConfig = { - dimensions: { - metrics, - buckets, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }, - }; - - if (visParams.showPartialRows && !visParams.showMetricsAtAllLevels) { - // Handle case where user wants to see partial rows but not metrics at all levels. - // This requires calculating how many metrics will come back in the tabified response, - // and removing all metrics from the dimensions except the last set. - const metricsPerBucket = metrics.length / buckets.length; - visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); - } - return visConfig; -}; - -export const toExpressionAstLegacy: VisToExpressionAst = (vis, params) => { - const esaggs = buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: vis.params.showPartialRows, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); - - const schemas = getVisSchemas(vis, params); - - const visConfig = { - ...vis.params, - ...buildTableVisConfig(schemas, vis.params), - title: vis.title, - }; - - const table = buildExpressionFunction('kibana_table', { - visConfig: JSON.stringify(visConfig), - }); - - const ast = buildExpression([esaggs, table]); - - return ast.toAst(); -}; diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts deleted file mode 100644 index a9cb22a056913..0000000000000 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ /dev/null @@ -1,121 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; -import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; - -import './index.scss'; - -import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; -import { TablePluginStartDependencies } from '../plugin'; -import { TableVisConfig, TableVisData } from '../types'; - -const innerAngularName = 'kibana/table_vis'; - -export type TableVisLegacyController = InstanceType< - ReturnType ->; - -export function getTableVisualizationControllerClass( - core: CoreSetup, - context: PluginInitializerContext -) { - return class TableVisualizationController { - tableVisModule: IModule | undefined; - injector: auto.IInjectorService | undefined; - el: JQuery; - $rootScope: IRootScopeService | null = null; - $scope: (IScope & { [key: string]: any }) | undefined; - $compile: ICompileService | undefined; - - constructor(domeElement: Element) { - this.el = $(domeElement); - } - - getInjector() { - if (!this.injector) { - const mountpoint = document.createElement('div'); - mountpoint.className = 'visualization'; - this.injector = angular.bootstrap(mountpoint, [innerAngularName]); - this.el.append(mountpoint); - } - - return this.injector; - } - - async initLocalAngular() { - if (!this.tableVisModule) { - const [coreStart, { kibanaLegacy }] = await core.getStartServices(); - await kibanaLegacy.loadAngularBootstrap(); - this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); - initTableVisLegacyModule(this.tableVisModule); - kibanaLegacy.loadFontAwesome(); - } - } - - async render( - esResponse: TableVisData, - visParams: TableVisConfig, - handlers: IInterpreterRenderHandlers - ): Promise { - await this.initLocalAngular(); - - return new Promise(async (resolve, reject) => { - try { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - - const updateScope = () => { - if (!this.$scope) { - return; - } - - this.$scope.visState = { - params: visParams, - title: visParams.title, - }; - this.$scope.esResponse = esResponse; - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = handlers.uiState; - this.$scope.filter = handlers.event; - updateScope(); - this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); - } - } catch (error) { - reject(error); - } - }); - } - - destroy() { - if (this.$rootScope) { - this.$rootScope.$destroy(); - this.$rootScope = null; - } - } - }; -} diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 0a9d477c26691..2ae8b68bba701 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -6,18 +6,14 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, AsyncPlugin } from 'kibana/public'; +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { DataPublicPluginStart } from '../../data/public'; import { setFormatService } from './services'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; - -interface ClientConfigType { - legacyVisEnabled: boolean; -} +import { registerTableVis } from './register_vis'; /** @internal */ export interface TablePluginSetupDependencies { @@ -29,31 +25,13 @@ export interface TablePluginSetupDependencies { /** @internal */ export interface TablePluginStartDependencies { data: DataPublicPluginStart; - kibanaLegacy: KibanaLegacyStart; } /** @internal */ export class TableVisPlugin - implements AsyncPlugin { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup( - core: CoreSetup, - deps: TablePluginSetupDependencies - ) { - const { legacyVisEnabled } = this.initializerContext.config.get(); - - if (legacyVisEnabled) { - const { registerLegacyVis } = await import('./legacy'); - registerLegacyVis(core, deps, this.initializerContext); - } else { - const { registerTableVis } = await import('./register_vis'); - registerTableVis(core, deps, this.initializerContext); - } + implements Plugin { + public setup(core: CoreSetup, deps: TablePluginSetupDependencies) { + registerTableVis(core, deps); } public start(core: CoreStart, { data }: TablePluginStartDependencies) { diff --git a/src/plugins/vis_type_table/public/register_vis.ts b/src/plugins/vis_type_table/public/register_vis.ts index cf15203b04864..b80dccccaff0a 100644 --- a/src/plugins/vis_type_table/public/register_vis.ts +++ b/src/plugins/vis_type_table/public/register_vis.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup } from 'kibana/public'; - +import { CoreSetup } from 'kibana/public'; import { TablePluginSetupDependencies, TablePluginStartDependencies } from './plugin'; import { createTableVisFn } from './table_vis_fn'; import { getTableVisRenderer } from './table_vis_renderer'; @@ -15,8 +14,7 @@ import { tableVisTypeDefinition } from './table_vis_type'; export const registerTableVis = async ( core: CoreSetup, - { expressions, visualizations }: TablePluginSetupDependencies, - context: PluginInitializerContext + { expressions, visualizations }: TablePluginSetupDependencies ) => { const [coreStart] = await core.getStartServices(); expressions.registerFunction(createTableVisFn); diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts index b3b20c22aaf52..b98fdd9c445db 100644 --- a/src/plugins/vis_type_table/server/index.ts +++ b/src/plugins/vis_type_table/server/index.ts @@ -13,9 +13,6 @@ import { configSchema, ConfigSchema } from '../config'; import { registerVisTypeTableUsageCollector } from './usage_collector'; export const config: PluginConfigDescriptor = { - exposeToBrowser: { - legacyVisEnabled: true, - }, schema: configSchema, deprecations: ({ renameFromRoot }) => [ renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'), diff --git a/test/functional/apps/visualize/legacy/_data_table.ts b/test/functional/apps/visualize/legacy/_data_table.ts deleted file mode 100644 index 6613e3d13a31b..0000000000000 --- a/test/functional/apps/visualize/legacy/_data_table.ts +++ /dev/null @@ -1,321 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects([ - 'visualize', - 'timePicker', - 'visEditor', - 'visChart', - 'legacyDataTableVis', - ]); - - describe('legacy data table visualization', function indexPatternCreation() { - before(async function () { - await PageObjects.visualize.initTests(); - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = Split rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should show percentage columns', async () => { - async function expectValidTableData() { - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['≥ 0B and < 1,000B', '1,351', '64.703%'], - ['≥ 1,000B and < 1.953KB', '737', '35.297%'], - ]); - } - - // load a plain table - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Range'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.clickGo(); - await PageObjects.visEditor.clickOptionsTab(); - await PageObjects.visEditor.setSelectByOptionText( - 'datatableVisualizationPercentageCol', - 'Count' - ); - await PageObjects.visEditor.clickGo(); - - await expectValidTableData(); - - // check that it works after selecting a column that's deleted - await PageObjects.visEditor.clickDataTab(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average', 'metrics'); - await PageObjects.visEditor.selectField('bytes', 'metrics'); - await PageObjects.visEditor.removeDimension(1); - await PageObjects.visEditor.clickGo(); - await PageObjects.visEditor.clickOptionsTab(); - - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['≥ 0B and < 1,000B', '344.094B'], - ['≥ 1,000B and < 1.953KB', '1.697KB'], - ]); - }); - - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['2015-09-20', '4,757'], - ['2015-09-21', '4,614'], - ['2015-09-22', '4,633'], - ]); - }); - - describe('otherBucket', () => { - before(async () => { - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.clickGo(); - - await PageObjects.visEditor.toggleOtherBucket(); - await PageObjects.visEditor.toggleMissingBucket(); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data', async () => { - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109'], - ['css', '2,159'], - ['Other', '2,736'], - ]); - }); - - it('should apply correct filter', async () => { - await PageObjects.legacyDataTableVis.filterOnTableCell(1, 3); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['png', '1,373'], - ['gif', '918'], - ['Other', '445'], - ]); - }); - }); - - describe('metricsOnAllLevels', () => { - before(async () => { - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.dest'); - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data without showMetricsAtAllLevels', async () => { - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', 'CN', '1,718'], - ['jpg', 'IN', '1,511'], - ['jpg', 'US', '770'], - ['jpg', 'ID', '314'], - ['jpg', 'PK', '244'], - ['css', 'CN', '422'], - ['css', 'IN', '346'], - ['css', 'US', '189'], - ['css', 'ID', '68'], - ['css', 'BR', '58'], - ]); - }); - - it('should show correct data without showMetricsAtAllLevels even if showPartialRows is selected', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showPartialRows', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', 'CN', '1,718'], - ['jpg', 'IN', '1,511'], - ['jpg', 'US', '770'], - ['jpg', 'ID', '314'], - ['jpg', 'PK', '244'], - ['css', 'CN', '422'], - ['css', 'IN', '346'], - ['css', 'US', '189'], - ['css', 'ID', '68'], - ['css', 'BR', '58'], - ]); - }); - - it('should show metrics on each level', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109', 'CN', '1,718'], - ['jpg', '9,109', 'IN', '1,511'], - ['jpg', '9,109', 'US', '770'], - ['jpg', '9,109', 'ID', '314'], - ['jpg', '9,109', 'PK', '244'], - ['css', '2,159', 'CN', '422'], - ['css', '2,159', 'IN', '346'], - ['css', '2,159', 'US', '189'], - ['css', '2,159', 'ID', '68'], - ['css', '2,159', 'BR', '58'], - ]); - }); - - it('should show metrics other than count on each level', async () => { - await PageObjects.visEditor.clickDataTab(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average', 'metrics'); - await PageObjects.visEditor.selectField('bytes', 'metrics'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109', '5.469KB', 'CN', '1,718', '5.477KB'], - ['jpg', '9,109', '5.469KB', 'IN', '1,511', '5.456KB'], - ['jpg', '9,109', '5.469KB', 'US', '770', '5.371KB'], - ['jpg', '9,109', '5.469KB', 'ID', '314', '5.424KB'], - ['jpg', '9,109', '5.469KB', 'PK', '244', '5.41KB'], - ['css', '2,159', '5.566KB', 'CN', '422', '5.712KB'], - ['css', '2,159', '5.566KB', 'IN', '346', '5.754KB'], - ['css', '2,159', '5.566KB', 'US', '189', '5.333KB'], - ['css', '2,159', '5.566KB', 'ID', '68', '4.82KB'], - ['css', '2,159', '5.566KB', 'BR', '58', '5.915KB'], - ]); - }); - }); - - describe('split tables', () => { - before(async () => { - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split table'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.dest'); - await PageObjects.visEditor.setSize(3, 3); - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.src'); - await PageObjects.visEditor.setSize(3, 4); - await PageObjects.visEditor.toggleOpenEditor(4, 'false'); - await PageObjects.visEditor.clickGo(); - }); - - it('should have a splitted table', async () => { - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - [ - ['CN', 'CN', '330'], - ['CN', 'IN', '274'], - ['CN', 'US', '140'], - ['IN', 'CN', '286'], - ['IN', 'IN', '281'], - ['IN', 'US', '133'], - ['US', 'CN', '135'], - ['US', 'IN', '134'], - ['US', 'US', '52'], - ], - [ - ['CN', 'CN', '90'], - ['CN', 'IN', '84'], - ['CN', 'US', '27'], - ['IN', 'CN', '69'], - ['IN', 'IN', '58'], - ['IN', 'US', '34'], - ['US', 'IN', '36'], - ['US', 'CN', '29'], - ['US', 'US', '13'], - ], - ]); - }); - - it('should show metrics for split bucket when using showMetricsAtAllLevels', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.legacyDataTableVis.getTableVisContent(); - expect(data).to.be.eql([ - [ - ['CN', '1,718', 'CN', '330'], - ['CN', '1,718', 'IN', '274'], - ['CN', '1,718', 'US', '140'], - ['IN', '1,511', 'CN', '286'], - ['IN', '1,511', 'IN', '281'], - ['IN', '1,511', 'US', '133'], - ['US', '770', 'CN', '135'], - ['US', '770', 'IN', '134'], - ['US', '770', 'US', '52'], - ], - [ - ['CN', '422', 'CN', '90'], - ['CN', '422', 'IN', '84'], - ['CN', '422', 'US', '27'], - ['IN', '346', 'CN', '69'], - ['IN', '346', 'IN', '58'], - ['IN', '346', 'US', '34'], - ['US', '189', 'IN', '36'], - ['US', '189', 'CN', '29'], - ['US', '189', 'US', '13'], - ], - ]); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/legacy/index.ts b/test/functional/apps/visualize/legacy/index.ts deleted file mode 100644 index 37cf8a5950592..0000000000000 --- a/test/functional/apps/visualize/legacy/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { FORMATS_UI_SETTINGS } from '../../../../../src/plugins/field_formats/common'; - -export default function ({ getPageObjects, getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['visualize']); - - describe('visualize with legacy visualizations', () => { - before(async () => { - await PageObjects.visualize.initTests(); - log.debug('Starting visualize legacy before method'); - await browser.setWindowSize(1280, 800); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - [FORMATS_UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', - }); - }); - - describe('legacy data table visualization', function () { - this.tags('ciGroup9'); - - loadTestFile(require.resolve('./_data_table')); - }); - }); -} diff --git a/test/functional/config.legacy.ts b/test/functional/config.legacy.ts deleted file mode 100644 index d38f30a32ef61..0000000000000 --- a/test/functional/config.legacy.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -// eslint-disable-next-line import/no-default-export -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const defaultConfig = await readConfigFile(require.resolve('./config')); - - return { - ...defaultConfig.getAll(), - - testFiles: [require.resolve('./apps/visualize/legacy')], - - kbnTestServer: { - ...defaultConfig.get('kbnTestServer'), - serverArgs: [ - ...defaultConfig.get('kbnTestServer.serverArgs'), - '--vis_type_table.legacyVisEnabled=true', - ], - }, - }; -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 61e89e8b37161..a8e690c69c951 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5023,12 +5023,7 @@ "visTypePie.pie.splitTitle": "チャートを分割", "visTypePie.valuesFormats.percent": "割合を表示", "visTypePie.valuesFormats.value": "値を表示", - "visTypeTable.aggTable.exportLabel": "エクスポート:", - "visTypeTable.aggTable.formattedLabel": "フォーマット済み", - "visTypeTable.aggTable.rawLabel": "未加工", "visTypeTable.defaultAriaLabel": "データ表ビジュアライゼーション", - "visTypeTable.directives.tableCellFilter.filterForValueTooltip": "値でフィルター", - "visTypeTable.directives.tableCellFilter.filterOutValueTooltip": "値を除外", "visTypeTable.function.adimension.buckets": "バケット", "visTypeTable.function.args.bucketsHelpText": "バケットディメンション構成", "visTypeTable.function.args.metricsHelpText": "メトリックディメンション構成", @@ -5076,7 +5071,6 @@ "visTypeTable.vis.controls.exportButtonLabel": "エクスポート", "visTypeTable.vis.controls.formattedCSVButtonLabel": "フォーマット済み", "visTypeTable.vis.controls.rawCSVButtonLabel": "未加工", - "visTypeTable.vis.noResultsFoundTitle": "結果が見つかりませんでした", "visTypeTagCloud.orientations.multipleText": "複数", "visTypeTagCloud.orientations.rightAngledText": "直角", "visTypeTagCloud.orientations.singleText": "単一", @@ -26901,4 +26895,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a9859b8618efd..822e172138abb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5068,12 +5068,7 @@ "visTypePie.pie.splitTitle": "拆分图表", "visTypePie.valuesFormats.percent": "显示百分比", "visTypePie.valuesFormats.value": "显示值", - "visTypeTable.aggTable.exportLabel": "导出:", - "visTypeTable.aggTable.formattedLabel": "格式化", - "visTypeTable.aggTable.rawLabel": "原始", "visTypeTable.defaultAriaLabel": "数据表可视化", - "visTypeTable.directives.tableCellFilter.filterForValueTooltip": "筛留值", - "visTypeTable.directives.tableCellFilter.filterOutValueTooltip": "筛除值", "visTypeTable.function.adimension.buckets": "存储桶", "visTypeTable.function.args.bucketsHelpText": "存储桶维度配置", "visTypeTable.function.args.metricsHelpText": "指标维度配置", @@ -5121,7 +5116,6 @@ "visTypeTable.vis.controls.exportButtonLabel": "导出", "visTypeTable.vis.controls.formattedCSVButtonLabel": "格式化", "visTypeTable.vis.controls.rawCSVButtonLabel": "原始", - "visTypeTable.vis.noResultsFoundTitle": "找不到结果", "visTypeTagCloud.orientations.multipleText": "多个", "visTypeTagCloud.orientations.rightAngledText": "直角", "visTypeTagCloud.orientations.singleText": "单个", @@ -27347,4 +27341,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file From 8a4355b35360499e63db3a825ccc8d5114425bc2 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 14 Sep 2021 12:36:08 +0300 Subject: [PATCH 06/39] [Visualize] Removes dimming opacity setting (#111704) * [Visualize] Removes the dimming opacity setting * Remove from docs * Fix 18n * Address PR comments * More cleaning Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/advanced-options.asciidoc | 4 ---- .../server/collectors/management/schema.ts | 4 ---- .../server/collectors/management/types.ts | 1 - src/plugins/telemetry/schema/oss_plugins.json | 6 ------ src/plugins/vis_types/vislib/common/index.ts | 1 - .../vislib/public/vislib/lib/dispatch.js | 15 +++------------ .../vis_types/vislib/public/vislib/vis.js | 3 +-- .../vis_types/vislib/server/ui_settings.ts | 19 +------------------ .../xy/public/components/xy_settings.tsx | 8 +------- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 11 files changed, 6 insertions(+), 59 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 6bf9ddb365290..872fcbbf83385 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -512,10 +512,6 @@ Enables the legacy charts library for timelion charts in Visualize. **This setting is deprecated and will not be supported as of 8.0.** Maps values to specific colors in charts using the *Compatibility* palette. -[[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: -The opacity of the chart items that are dimmed when highlighting another element -of the chart. Use numbers between 0 and 1. The lower the number, the more the highlighted element stands out. - [[visualization-heatmap-maxbuckets]]`visualization:heatmap:maxBuckets`:: The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance. diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 237ec54e4692b..bc38b63730b20 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -68,10 +68,6 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'visualization:dimmingOpacity': { - type: 'float', - _meta: { description: 'Non-default value of setting.' }, - }, 'visualization:tileMap:maxPrecision': { type: 'long', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 0c4b848ff3544..24a20b458c789 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -40,7 +40,6 @@ export interface UsageStats { 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; 'visualization:regionmap:showWarnings': boolean; - 'visualization:dimmingOpacity': number; 'visualization:tileMap:maxPrecision': number; 'csv:separator': string; 'visualization:tileMap:WMSdefaults': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index abf4aac7df59a..25c2f37ea04c7 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7068,12 +7068,6 @@ "description": "Non-default value of setting." } }, - "visualization:dimmingOpacity": { - "type": "float", - "_meta": { - "description": "Non-default value of setting." - } - }, "visualization:tileMap:maxPrecision": { "type": "long", "_meta": { diff --git a/src/plugins/vis_types/vislib/common/index.ts b/src/plugins/vis_types/vislib/common/index.ts index ad560e0a3023c..6daae0548c038 100644 --- a/src/plugins/vis_types/vislib/common/index.ts +++ b/src/plugins/vis_types/vislib/common/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export const DIMMING_OPACITY_SETTING = 'visualization:dimmingOpacity'; export const HEATMAP_MAX_BUCKETS_SETTING = 'visualization:heatmap:maxBuckets'; diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.js index 30fd0f1fcbbd2..e93ed922b3fd1 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.js @@ -9,7 +9,6 @@ import d3 from 'd3'; import { get, pull, rest, size, reduce } from 'lodash'; import $ from 'jquery'; -import { DIMMING_OPACITY_SETTING } from '../../../common'; /** * Handles event responses @@ -280,7 +279,7 @@ export class Dispatch { const addEvent = this.addEvent; const $el = this.handler.el; if (!this.handler.highlight) { - this.handler.highlight = self.getHighlighter(self.uiSettings); + this.handler.highlight = self.getHighlighter(); } function hover(d, i) { @@ -375,20 +374,18 @@ export class Dispatch { /** * return function to Highlight the element that is under the cursor * by reducing the opacity of all the elements on the graph. - * @param uiSettings * @method getHighlighter */ - getHighlighter(uiSettings) { + getHighlighter() { return function highlight(element) { const label = this.getAttribute('data-label'); if (!label) return; - const dimming = uiSettings.get(DIMMING_OPACITY_SETTING); $(element) .parent() .find('[data-label]') .css('opacity', 1) //Opacity 1 is needed to avoid the css application .not((els, el) => String($(el).data('label')) === label) - .css('opacity', justifyOpacity(dimming)); + .css('opacity', 0.5); }; } @@ -470,9 +467,3 @@ export class Dispatch { function validBrushClick(event) { return event.button === 0; } - -function justifyOpacity(opacity) { - const decimalNumber = parseFloat(opacity, 10); - const fallbackOpacity = 0.5; - return 0 <= decimalNumber && decimalNumber <= 1 ? decimalNumber : fallbackOpacity; -} diff --git a/src/plugins/vis_types/vislib/public/vislib/vis.js b/src/plugins/vis_types/vislib/public/vislib/vis.js index 2afe5ff8281ee..790dc1ecfa2d4 100644 --- a/src/plugins/vis_types/vislib/public/vislib/vis.js +++ b/src/plugins/vis_types/vislib/public/vislib/vis.js @@ -13,7 +13,7 @@ import { EventEmitter } from 'events'; import { VislibError } from './errors'; import { VisConfig } from './lib/vis_config'; import { Handler } from './lib/handler'; -import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../../common'; +import { HEATMAP_MAX_BUCKETS_SETTING } from '../../common'; /** * Creates the visualizations. @@ -28,7 +28,6 @@ export class Vis extends EventEmitter { super(); this.element = element.get ? element.get(0) : element; this.visConfigArgs = _.cloneDeep(visConfigArgs); - this.visConfigArgs.dimmingOpacity = core.uiSettings.get(DIMMING_OPACITY_SETTING); this.visConfigArgs.heatmapMaxBuckets = core.uiSettings.get(HEATMAP_MAX_BUCKETS_SETTING); this.charts = charts; this.uiSettings = core.uiSettings; diff --git a/src/plugins/vis_types/vislib/server/ui_settings.ts b/src/plugins/vis_types/vislib/server/ui_settings.ts index bd4615e47fb6e..1c7a7cdedc4e0 100644 --- a/src/plugins/vis_types/vislib/server/ui_settings.ts +++ b/src/plugins/vis_types/vislib/server/ui_settings.ts @@ -10,26 +10,9 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from 'kibana/server'; -import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../common'; +import { HEATMAP_MAX_BUCKETS_SETTING } from '../common'; export const getUiSettings: () => Record = () => ({ - // TODO: move this to vis_type_xy when vislib is removed - // https://github.com/elastic/kibana/issues/56143 - [DIMMING_OPACITY_SETTING]: { - name: i18n.translate('visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle', { - defaultMessage: 'Dimming opacity', - }), - value: 0.5, - type: 'number', - description: i18n.translate('visTypeVislib.advancedSettings.visualization.dimmingOpacityText', { - defaultMessage: - 'The opacity of the chart items that are dimmed when highlighting another element of the chart. ' + - 'The lower this number, the more the highlighted element will stand out. ' + - 'This must be a number between 0 and 1.', - }), - category: ['visualization'], - schema: schema.number(), - }, [HEATMAP_MAX_BUCKETS_SETTING]: { name: i18n.translate('visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle', { defaultMessage: 'Heatmap maximum buckets', diff --git a/src/plugins/vis_types/xy/public/components/xy_settings.tsx b/src/plugins/vis_types/xy/public/components/xy_settings.tsx index 92b47edccfd92..5e02b65822d6c 100644 --- a/src/plugins/vis_types/xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_types/xy/public/components/xy_settings.tsx @@ -28,7 +28,7 @@ import { import { renderEndzoneTooltip } from '../../../../charts/public'; -import { getThemeService, getUISettings } from '../services'; +import { getThemeService } from '../services'; import { VisConfig } from '../types'; declare global { @@ -101,16 +101,10 @@ export const XYSettings: FC = ({ const themeService = getThemeService(); const theme = themeService.useChartsTheme(); const baseTheme = themeService.useChartsBaseTheme(); - const dimmingOpacity = getUISettings().get('visualization:dimmingOpacity'); const valueLabelsStyling = getValueLabelsStyling(); const themeOverrides: PartialTheme = { markSizeRatio, - sharedStyle: { - unhighlighted: { - opacity: dimmingOpacity, - }, - }, barSeriesStyle: { ...valueLabelsStyling, }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a8e690c69c951..48db2a46460a2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5653,8 +5653,6 @@ "visTypeVega.vegaParser.widthAndHeightParamsAreRequired": "{autoSizeParam}が{noneParam}に設定されているときには、カットまたは繰り返された{vegaLiteParam}仕様を使用している間に何も表示されません。修正するには、{autoSizeParam}を削除するか、{vegaParam}を使用してください。", "visTypeVega.visualization.renderErrorTitle": "Vega エラー", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "データなしにはレンダリングできません", - "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "チャートの別のエレメントが選択された時に暗くなるチャート項目の透明度です。この数字が小さければ小さいほど、ハイライトされたエレメントが目立ちます。0と1の間の数字で設定します。", - "visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle": "減光透明度", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "1つのデータソースが返せるバケットの最大数です。値が大きいとブラウザのレンダリング速度が下がる可能性があります。", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "ヒートマップの最大バケット数", "visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 822e172138abb..a4d731cebf0bd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5699,8 +5699,6 @@ "visTypeVega.vegaParser.widthAndHeightParamsAreRequired": "{autoSizeParam} 设置为 {noneParam} 时,如果使用分面或重复 {vegaLiteParam} 规格,将不会呈现任何内容。要解决问题,请移除 {autoSizeParam} 或使用 {vegaParam}。", "visTypeVega.visualization.renderErrorTitle": "Vega 错误", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "没有数据时无法渲染", - "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "突出显示图表的其他元素时变暗图表项的透明度。此数字越低,突出显示的元素越突出。必须是介于 0 和 1 之间的数字。", - "visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle": "变暗透明度", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "单个数据源可以返回的最大存储桶数目。较高的数目可能对浏览器呈现性能有负面影响", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "热图最大存储桶数", "visTypeVislib.aggResponse.allDocsTitle": "所有文档", From 4a2acad218e0d7700edd12dd6b53b0fbe73d9567 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 14 Sep 2021 12:52:33 +0300 Subject: [PATCH 07/39] Functional tests for execution context propagation in alerting and task manager plugins (#111179) * move execution context tests to x-pack folder * add execution context tests for alerts and tasks * cleanup tests * remove obsolete test folder * use ToolingLog instead of console.log --- scripts/functional_tests.js | 1 - test/functional_execution_context/services.ts | 11 - .../tests/execution_context.ts | 374 ----------------- x-pack/scripts/functional_tests.js | 1 + .../functional_execution_context/config.ts | 39 +- .../fixtures/plugins/alerts/kibana.json | 13 + .../fixtures/plugins/alerts/package.json | 13 + .../fixtures/plugins/alerts/server/index.ts | 10 + .../fixtures/plugins/alerts/server/plugin.ts | 88 ++++ .../ftr_provider_context.ts | 7 +- .../functional_execution_context/services.ts | 10 + .../test_utils.ts | 52 +++ .../tests/browser.ts | 381 ++++++++++++++++++ .../tests/index.ts | 8 +- .../tests/server.ts | 112 +++++ 15 files changed, 715 insertions(+), 405 deletions(-) delete mode 100644 test/functional_execution_context/services.ts delete mode 100644 test/functional_execution_context/tests/execution_context.ts rename {test => x-pack/test}/functional_execution_context/config.ts (51%) create mode 100644 x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json create mode 100644 x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json create mode 100644 x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts create mode 100644 x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts rename {test => x-pack/test}/functional_execution_context/ftr_provider_context.ts (60%) create mode 100644 x-pack/test/functional_execution_context/services.ts create mode 100644 x-pack/test/functional_execution_context/test_utils.ts create mode 100644 x-pack/test/functional_execution_context/tests/browser.ts rename {test => x-pack/test}/functional_execution_context/tests/index.ts (60%) create mode 100644 x-pack/test/functional_execution_context/tests/server.ts diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index ce62face43906..032a28477bc90 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -19,7 +19,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config.js'), require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/examples/config.js'), - require.resolve('../test/functional_execution_context/config.ts'), ]; require('../src/setup_node_env'); diff --git a/test/functional_execution_context/services.ts b/test/functional_execution_context/services.ts deleted file mode 100644 index b0cf94fedd749..0000000000000 --- a/test/functional_execution_context/services.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { services as functionalServices } from '../functional/services'; - -export const services = functionalServices; diff --git a/test/functional_execution_context/tests/execution_context.ts b/test/functional_execution_context/tests/execution_context.ts deleted file mode 100644 index ad9b4332c9f02..0000000000000 --- a/test/functional_execution_context/tests/execution_context.ts +++ /dev/null @@ -1,374 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import type { Ecs, KibanaExecutionContext } from 'kibana/server'; - -import Fs from 'fs/promises'; -import Path from 'path'; -import { isEqual } from 'lodash'; -import type { FtrProviderContext } from '../ftr_provider_context'; - -const logFilePath = Path.resolve(__dirname, '../kibana.log'); - -// to avoid splitting log record containing \n symbol -const endOfLine = /(?<=})\s*\n/; -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home']); - const retry = getService('retry'); - - async function assertLogContains( - description: string, - predicate: (record: Ecs) => boolean - ): Promise { - // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. - await retry.waitFor(description, async () => { - const logsStr = await Fs.readFile(logFilePath, 'utf-8'); - const normalizedRecords = logsStr - .split(endOfLine) - .filter(Boolean) - .map((s) => JSON.parse(s)); - - return normalizedRecords.some(predicate); - }); - } - - function isExecutionContextLog( - record: string | undefined, - executionContext: KibanaExecutionContext - ) { - if (!record) return false; - try { - const object = JSON.parse(record); - return isEqual(object, executionContext); - } catch (e) { - return false; - } - } - - describe('Execution context service', () => { - before(async () => { - await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await PageObjects.home.addSampleDataSet('flights'); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.home.removeSampleDataSet('flights'); - }); - - describe('discover app', () => { - before(async () => { - await PageObjects.common.navigateToApp('discover'); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - it('propagates context for Discover', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => Boolean(record.http?.request?.id?.includes('kibana:application:discover')) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - description: 'fetch documents', - id: '', - name: 'discover', - type: 'application', - // discovery doesn't have an URL since one of from the example dataset is not saved separately - url: '/app/discover', - }) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - description: 'fetch chart data and total hits', - id: '', - name: 'discover', - type: 'application', - url: '/app/discover', - }) - ); - }); - }); - - describe('dashboard app', () => { - before(async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); - await PageObjects.dashboard.waitForRenderComplete(); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - describe('propagates context for Lens visualizations', () => { - it('lnsXY', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsXY', - id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', - description: '[Flights] Flight count', - url: '/app/lens#/edit_by_value', - }) - ); - }); - - it('lnsMetric', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsMetric', - id: '2e33ade5-96e5-40b4-b460-493e5d4fa834', - description: '', - url: '/app/lens#/edit_by_value', - }) - ); - }); - - it('lnsDatatable', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsDatatable', - id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', - description: 'Cities by delay, cancellation', - url: '/app/lens#/edit_by_value', - }) - ); - }); - - it('lnsPie', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' - ) - ) - ); - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsPie', - id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', - description: '[Flights] Delay Type', - url: '/app/lens#/edit_by_value', - }) - ); - }); - }); - - it('propagates context for built-in Discover', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' - ) - ) - ); - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'search', - name: 'discover', - id: '571aaf70-4c88-11e8-b3d7-01146121b73d', - description: '[Flights] Flight Log', - url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', - }) - ); - }); - - it('propagates context for TSVB visualizations', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:TSVB:bcb63b50-4c89-11e8-b3d7-01146121b73d' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'TSVB', - id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', - description: '[Flights] Delays & Cancellations', - url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', - }) - ); - }); - - it('propagates context for Vega visualizations', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'Vega', - id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', - description: '[Flights] Airport Connections (Hover Over Airport)', - url: '/app/visualize#/edit/ed78a660-53a0-11e8-acbd-0be0ad9d822b', - }) - ); - }); - - it('propagates context for Tag Cloud visualization', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Tag cloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'Tag cloud', - id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', - description: '[Flights] Destination Weather', - url: '/app/visualize#/edit/293b5a30-4c8f-11e8-b3d7-01146121b73d', - }) - ); - }); - - it('propagates context for Vertical bar visualization', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vertical bar:9886b410-4c8b-11e8-b3d7-01146121b73d' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'Vertical bar', - id: '9886b410-4c8b-11e8-b3d7-01146121b73d', - description: '[Flights] Delay Buckets', - url: '/app/visualize#/edit/9886b410-4c8b-11e8-b3d7-01146121b73d', - }) - ); - }); - }); - }); -} diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index b023ebb44cc38..3c1cdd5790f3c 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -90,6 +90,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), require.resolve('../test/examples/config.ts'), require.resolve('../test/performance/config.ts'), + require.resolve('../test/functional_execution_context/config.ts'), ]; require('../../src/setup_node_env'); diff --git a/test/functional_execution_context/config.ts b/x-pack/test/functional_execution_context/config.ts similarity index 51% rename from test/functional_execution_context/config.ts rename to x-pack/test/functional_execution_context/config.ts index 6e46189073001..f841e8957cde3 100644 --- a/test/functional_execution_context/config.ts +++ b/x-pack/test/functional_execution_context/config.ts @@ -1,16 +1,26 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ - -import { FtrConfigProviderContext } from '@kbn/test'; import Path from 'path'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; +import { logFilePath } from './test_utils'; + +const alertTestPlugin = Path.resolve(__dirname, './fixtures/plugins/alerts'); export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../../test/functional/config')); + + const servers = { + ...functionalConfig.get('servers'), + elasticsearch: { + ...functionalConfig.get('servers.elasticsearch'), + protocol: 'https', + }, + }; return { ...functionalConfig.getAll(), @@ -19,24 +29,31 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { junit: { reportName: 'Execution Context Functional Tests', }, + servers, + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + ssl: true, + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${alertTestPlugin}`, + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + '--execution_context.enabled=true', '--logging.appenders.file.type=file', - `--logging.appenders.file.fileName=${Path.resolve(__dirname, './kibana.log')}`, + `--logging.appenders.file.fileName=${logFilePath}`, '--logging.appenders.file.layout.type=json', '--logging.loggers[0].name=elasticsearch.query', '--logging.loggers[0].level=all', - // eslint-disable-next-line prettier/prettier - '--logging.loggers[0].appenders=[\"file\"]', + `--logging.loggers[0].appenders=${JSON.stringify(['file'])}`, '--logging.loggers[1].name=execution_context', '--logging.loggers[1].level=debug', - // eslint-disable-next-line prettier/prettier - '--logging.loggers[1].appenders=[\"file\"]', + `--logging.loggers[1].appenders=${JSON.stringify(['file'])}`, ], }, }; diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json new file mode 100644 index 0000000000000..7a51160f20041 --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "alertsFixtures", + "owner": { + "name": "Core team", + "githubTeam": "kibana-core" + }, + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features", "alerting"], + "server": true, + "ui": false +} diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json new file mode 100644 index 0000000000000..ac456b01d3493 --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json @@ -0,0 +1,13 @@ +{ + "name": "alerts-fixtures", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "scripts": { + "kbn": "node ../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts new file mode 100644 index 0000000000000..700aee6bfd49d --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts new file mode 100644 index 0000000000000..47a9e4edc30fc --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; +import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../plugins/alerting/server/plugin'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../plugins/security/server'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; + alerting: AlertingPluginSetup; +} + +export interface FixtureStartDeps { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; +} + +export class FixturePlugin implements Plugin { + constructor() {} + + public setup(core: CoreSetup, { features, alerting }: FixtureSetupDeps) { + features.registerKibanaFeature({ + id: 'alertsFixture', + name: 'Alerts', + app: ['alerts', 'kibana'], + category: { id: 'foo', label: 'foo' }, + alerting: ['test.executionContext'], + privileges: { + all: { + app: ['alerts', 'kibana'], + savedObject: { + all: ['alert'], + read: [], + }, + alerting: { + rule: { + all: ['test.executionContext'], + }, + }, + ui: [], + }, + read: { + app: ['alerts', 'kibana'], + savedObject: { + all: [], + read: ['alert'], + }, + alerting: { + rule: { + read: ['test.executionContext'], + }, + }, + ui: [], + }, + }, + }); + + alerting.registerType({ + id: 'test.executionContext', + name: 'Test: Query Elasticsearch server', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() { + const [coreStart] = await core.getStartServices(); + await coreStart.elasticsearch.client.asInternalUser.ping(); + }, + }); + } + + public start() {} + public stop() {} +} diff --git a/test/functional_execution_context/ftr_provider_context.ts b/x-pack/test/functional_execution_context/ftr_provider_context.ts similarity index 60% rename from test/functional_execution_context/ftr_provider_context.ts rename to x-pack/test/functional_execution_context/ftr_provider_context.ts index d4ac701735efb..c5aadf858692a 100644 --- a/test/functional_execution_context/ftr_provider_context.ts +++ b/x-pack/test/functional_execution_context/ftr_provider_context.ts @@ -1,13 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { GenericFtrProviderContext } from '@kbn/test'; -import { pageObjects } from '../functional/page_objects'; +import { pageObjects } from '../../test/functional/page_objects'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional_execution_context/services.ts b/x-pack/test/functional_execution_context/services.ts new file mode 100644 index 0000000000000..e0aaa899deabf --- /dev/null +++ b/x-pack/test/functional_execution_context/services.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { services as functionalServices } from '../../test/functional/services'; + +export const services = functionalServices; diff --git a/x-pack/test/functional_execution_context/test_utils.ts b/x-pack/test/functional_execution_context/test_utils.ts new file mode 100644 index 0000000000000..94750fa55e964 --- /dev/null +++ b/x-pack/test/functional_execution_context/test_utils.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import Fs from 'fs/promises'; +import Path from 'path'; +import { isEqualWith } from 'lodash'; +import type { Ecs, KibanaExecutionContext } from 'kibana/server'; +import type { RetryService } from '../../../test/common/services/retry'; + +export const logFilePath = Path.resolve(__dirname, './kibana.log'); +export const ANY = Symbol('any'); + +export function isExecutionContextLog( + record: string | undefined, + executionContext: KibanaExecutionContext +) { + if (!record) return false; + try { + const object = JSON.parse(record); + return isEqualWith(object, executionContext, function customizer(obj1: any, obj2: any) { + if (obj2 === ANY) return true; + }); + } catch (e) { + return false; + } +} + +// to avoid splitting log record containing \n symbol +const endOfLine = /(?<=})\s*\n/; +export async function assertLogContains({ + description, + predicate, + retry, +}: { + description: string; + predicate: (record: Ecs) => boolean; + retry: RetryService; +}): Promise { + // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. + await retry.waitFor(description, async () => { + const logsStr = await Fs.readFile(logFilePath, 'utf-8'); + const normalizedRecords = logsStr + .split(endOfLine) + .filter(Boolean) + .map((s) => JSON.parse(s)); + + return normalizedRecords.some(predicate); + }); +} diff --git a/x-pack/test/functional_execution_context/tests/browser.ts b/x-pack/test/functional_execution_context/tests/browser.ts new file mode 100644 index 0000000000000..9e927dd2bc171 --- /dev/null +++ b/x-pack/test/functional_execution_context/tests/browser.ts @@ -0,0 +1,381 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../ftr_provider_context'; +import { assertLogContains, isExecutionContextLog } from '../test_utils'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home']); + const retry = getService('retry'); + + describe('Browser apps', () => { + before(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.removeSampleDataSet('flights'); + }); + + describe('discover app', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('propagates context for Discover', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean(record.http?.request?.id?.includes('kibana:application:discover')), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + description: 'fetch documents', + id: '', + name: 'discover', + type: 'application', + // discovery doesn't have an URL since one of from the example dataset is not saved separately + url: '/app/discover', + }), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + description: 'fetch chart data and total hits', + id: '', + name: 'discover', + type: 'application', + url: '/app/discover', + }), + retry, + }); + }); + }); + + describe('dashboard app', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + describe('propagates context for Lens visualizations', () => { + it('lnsXY', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsXY', + id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', + description: '[Flights] Flight count', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + + it('lnsMetric', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsMetric', + id: '2e33ade5-96e5-40b4-b460-493e5d4fa834', + description: '', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + + it('lnsDatatable', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsDatatable', + id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', + description: 'Cities by delay, cancellation', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + + it('lnsPie', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' + ) + ), + retry, + }); + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsPie', + id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', + description: '[Flights] Delay Type', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + }); + + it('propagates context for built-in Discover', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'search', + name: 'discover', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + description: '[Flights] Flight Log', + url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + + it('propagates context for TSVB visualizations', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:TSVB:bcb63b50-4c89-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'TSVB', + id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', + description: '[Flights] Delays & Cancellations', + url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + + it('propagates context for Vega visualizations', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vega', + id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', + description: '[Flights] Airport Connections (Hover Over Airport)', + url: '/app/visualize#/edit/ed78a660-53a0-11e8-acbd-0be0ad9d822b', + }), + retry, + }); + }); + + it('propagates context for Tag Cloud visualization', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Tag cloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Tag cloud', + id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', + description: '[Flights] Destination Weather', + url: '/app/visualize#/edit/293b5a30-4c8f-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + + it('propagates context for Vertical bar visualization', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vertical bar:9886b410-4c8b-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vertical bar', + id: '9886b410-4c8b-11e8-b3d7-01146121b73d', + description: '[Flights] Delay Buckets', + url: '/app/visualize#/edit/9886b410-4c8b-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + }); + }); +} diff --git a/test/functional_execution_context/tests/index.ts b/x-pack/test/functional_execution_context/tests/index.ts similarity index 60% rename from test/functional_execution_context/tests/index.ts rename to x-pack/test/functional_execution_context/tests/index.ts index 6dc92f6fb3c8b..6d74a94608671 100644 --- a/test/functional_execution_context/tests/index.ts +++ b/x-pack/test/functional_execution_context/tests/index.ts @@ -1,9 +1,8 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { FtrProviderContext } from '../ftr_provider_context'; @@ -11,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Execution context', function () { this.tags('ciGroup1'); - loadTestFile(require.resolve('./execution_context')); + loadTestFile(require.resolve('./browser')); + loadTestFile(require.resolve('./server')); }); } diff --git a/x-pack/test/functional_execution_context/tests/server.ts b/x-pack/test/functional_execution_context/tests/server.ts new file mode 100644 index 0000000000000..8997c83f4f696 --- /dev/null +++ b/x-pack/test/functional_execution_context/tests/server.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils'; + +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export default function ({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + + async function waitForStatus( + id: string, + statuses: Set, + waitMillis: number = 10000 + ): Promise> { + if (waitMillis < 0) { + expect().fail(`waiting for alert ${id} statuses ${Array.from(statuses)} timed out`); + } + + const response = await supertest.get(`/api/alerting/rule/${id}`); + expect(response.status).to.eql(200); + const { status } = response.body.execution_status; + if (statuses.has(status)) return response.body.execution_status; + + log.debug( + `waitForStatus(${Array.from(statuses)} for id:${id}): got ${JSON.stringify( + response.body.execution_status + )}, retrying` + ); + + const WaitForStatusIncrement = 500; + await delay(WaitForStatusIncrement); + return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement); + } + + describe('Server-side apps', () => { + it('propagates context for Task and Alerts', async () => { + const { body: createdAlert } = await supertest + .post('/api/alerting/rule') + .set('kbn-xsrf', 'true') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + rule_type_id: 'test.executionContext', + consumer: 'alertsFixture', + schedule: { interval: '3s' }, + throttle: '20s', + actions: [], + params: {}, + notify_when: 'onThrottleInterval', + }) + .expect(200); + + const alertId = createdAlert.id; + + await waitForStatus(alertId, new Set(['ok']), 90_000); + + await assertLogContains({ + description: + 'task manager execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + // exclude part with taskId + record.http?.request?.id?.includes( + `kibana:task manager:run alerting:test.executionContext:` + ) + ), + retry, + }); + + await assertLogContains({ + description: + 'alerting execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes(`alert:execute test.executionContext:${alertId}`) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'task manager', + name: 'run alerting:test.executionContext', + // @ts-expect-error. it accepts strings only + id: ANY, + description: 'run task', + }, + type: 'alert', + name: 'execute test.executionContext', + id: alertId, + description: 'execute [test.executionContext] with name [abc] in [default] namespace', + }), + retry, + }); + }); + }); +} From a55c39b62421e358bfa7ce78b4f84fc1cc47d391 Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Tue, 14 Sep 2021 12:59:50 +0200 Subject: [PATCH 08/39] Show process crawls together with crawl requests (#111398) --- .../add_domain/add_domain_logic.test.ts | 6 + .../components/crawl_requests_table.test.tsx | 9 +- .../components/crawl_requests_table.tsx | 34 ++- .../manage_crawls_popover_logic.ts | 3 + .../components/crawler/crawler_logic.test.ts | 259 ++++++++---------- .../components/crawler/crawler_logic.ts | 106 +++---- .../crawler/crawler_overview.test.tsx | 13 +- .../components/crawler/crawler_overview.tsx | 4 +- .../crawler/crawler_overview_logic.test.ts | 2 + .../crawler/crawler_router.test.tsx | 2 - .../components/crawler/crawler_router.tsx | 3 +- .../crawler/crawler_single_domain.test.tsx | 1 - .../app_search/components/crawler/types.ts | 24 ++ .../components/crawler/utils.test.ts | 36 ++- .../app_search/components/crawler/utils.ts | 27 +- 15 files changed, 298 insertions(+), 231 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts index 633c8de5e5655..4dd699a733b8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts @@ -304,6 +304,8 @@ describe('AddDomainLogic', () => { http.post.mockReturnValueOnce( Promise.resolve({ domains: [], + events: [], + most_recent_crawl_request: null, }) ); @@ -312,6 +314,8 @@ describe('AddDomainLogic', () => { expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith({ domains: [], + events: [], + mostRecentCrawlRequest: null, }); }); @@ -328,6 +332,8 @@ describe('AddDomainLogic', () => { name: 'https://swiftype.co/site-search', }, ], + events: [], + most_recent_crawl_request: null, }) ); jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainSuccess'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx index 13a7c641822b9..b36b92bc42847 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx @@ -16,16 +16,17 @@ import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui'; import { mountWithIntl } from '../../../../test_helpers'; -import { CrawlerStatus, CrawlRequest } from '../types'; +import { CrawlEvent, CrawlerStatus } from '../types'; import { CrawlRequestsTable } from './crawl_requests_table'; -const values: { crawlRequests: CrawlRequest[] } = { +const values: { events: CrawlEvent[] } = { // CrawlerLogic - crawlRequests: [ + events: [ { id: '618d0e66abe97bc688328900', status: CrawlerStatus.Pending, + stage: 'crawl', createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', beganAt: null, completedAt: null, @@ -69,7 +70,7 @@ describe('CrawlRequestsTable', () => { it('displays an empty prompt when there are no crawl requests', () => { setMockValues({ ...values, - crawlRequests: [], + events: [], }); wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx index 8a2b08878ff78..6d14e35946adf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx @@ -9,16 +9,21 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiBasicTable, EuiEmptyPrompt, EuiTableFieldDataColumnType } from '@elastic/eui'; +import { + EuiBasicTable, + EuiEmptyPrompt, + EuiIconTip, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CrawlerLogic } from '../crawler_logic'; -import { CrawlRequest, readableCrawlerStatuses } from '../types'; +import { CrawlEvent, readableCrawlerStatuses } from '../types'; import { CustomFormattedTimestamp } from './custom_formatted_timestamp'; -const columns: Array> = [ +const columns: Array> = [ { field: 'id', name: i18n.translate( @@ -36,7 +41,7 @@ const columns: Array> = [ defaultMessage: 'Created', } ), - render: (createdAt: CrawlRequest['createdAt']) => ( + render: (createdAt: CrawlEvent['createdAt']) => ( ), }, @@ -48,17 +53,32 @@ const columns: Array> = [ defaultMessage: 'Status', } ), - render: (status: CrawlRequest['status']) => readableCrawlerStatuses[status], + align: 'right', + render: (status: CrawlEvent['status'], event: CrawlEvent) => ( + <> + {event.stage === 'process' && ( + + )} + {readableCrawlerStatuses[status]} + + ), }, ]; export const CrawlRequestsTable: React.FC = () => { - const { crawlRequests } = useValues(CrawlerLogic); + const { events } = useValues(CrawlerLogic); return ( { availableDeduplicationFields: ['title', 'description'], }, ], + events: [ + { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + stage: 'crawl', + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + ], + mostRecentCrawlRequest: { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, }; beforeEach(() => { @@ -127,32 +146,16 @@ describe('CrawlerLogic', () => { it('should set all received data as top-level values', () => { expect(CrawlerLogic.values.domains).toEqual(crawlerData.domains); + expect(CrawlerLogic.values.events).toEqual(crawlerData.events); + expect(CrawlerLogic.values.mostRecentCrawlRequest).toEqual( + crawlerData.mostRecentCrawlRequest + ); }); it('should set dataLoading to false', () => { expect(CrawlerLogic.values.dataLoading).toEqual(false); }); }); - - describe('onReceiveCrawlRequests', () => { - const crawlRequests: CrawlRequest[] = [ - { - id: '618d0e66abe97bc688328900', - status: CrawlerStatus.Pending, - createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - ]; - - beforeEach(() => { - CrawlerLogic.actions.onReceiveCrawlRequests(crawlRequests); - }); - - it('should set the crawl requests', () => { - expect(CrawlerLogic.values.crawlRequests).toEqual(crawlRequests); - }); - }); }); describe('listeners', () => { @@ -170,20 +173,90 @@ describe('CrawlerLogic', () => { ); }); + it('creates a new timeout when there is an active process crawl', async () => { + jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + most_recent_crawl_request: null, + events: [ + { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Running, + stage: 'process', + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + ], + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.createNewTimeoutForCrawlerData).toHaveBeenCalled(); + }); + + describe('on success', () => { + [ + CrawlerStatus.Pending, + CrawlerStatus.Starting, + CrawlerStatus.Running, + CrawlerStatus.Canceling, + ].forEach((status) => { + it(`creates a new timeout for status ${status}`, async () => { + jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + most_recent_crawl_request: { status }, + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.createNewTimeoutForCrawlerData).toHaveBeenCalled(); + }); + }); + + [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].forEach((status) => { + it(`clears the timeout and fetches data for status ${status}`, async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + most_recent_crawl_request: { status }, + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + }); + }); + }); + it('calls flashApiErrors when there is an error on the request for crawler data', async () => { + jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); http.get.mockReturnValueOnce(Promise.reject('error')); CrawlerLogic.actions.fetchCrawlerData(); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(CrawlerLogic.actions.createNewTimeoutForCrawlerData).toHaveBeenCalled(); }); }); describe('startCrawl', () => { describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawl requests', async () => { - jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + it('creates a new crawl request and then fetches the latest crawler data', async () => { + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.startCrawl(); @@ -192,7 +265,7 @@ describe('CrawlerLogic', () => { expect(http.post).toHaveBeenCalledWith( '/internal/app_search/engines/some-engine/crawler/crawl_requests' ); - expect(CrawlerLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); @@ -210,8 +283,8 @@ describe('CrawlerLogic', () => { describe('stopCrawl', () => { describe('success path', () => { - it('stops the crawl starts and then fetches the latest crawl requests', async () => { - jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + it('stops the crawl starts and then fetches the latest crawler data', async () => { + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.stopCrawl(); @@ -220,13 +293,13 @@ describe('CrawlerLogic', () => { expect(http.post).toHaveBeenCalledWith( '/internal/app_search/engines/some-engine/crawler/crawl_requests/cancel' ); - expect(CrawlerLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); describe('on failure', () => { it('flashes an error message', async () => { - jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); http.post.mockReturnValueOnce(Promise.reject('error')); CrawlerLogic.actions.stopCrawl(); @@ -237,19 +310,19 @@ describe('CrawlerLogic', () => { }); }); - describe('createNewTimeoutForCrawlRequests', () => { + describe('createNewTimeoutForCrawlerData', () => { it('saves the timeout ID in the logic', () => { jest.spyOn(CrawlerLogic.actions, 'onCreateNewTimeout'); - jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); - CrawlerLogic.actions.createNewTimeoutForCrawlRequests(2000); + CrawlerLogic.actions.createNewTimeoutForCrawlerData(2000); expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000); expect(CrawlerLogic.actions.onCreateNewTimeout).toHaveBeenCalled(); jest.runAllTimers(); - expect(CrawlerLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); it('clears a timeout if one already exists', () => { @@ -258,130 +331,32 @@ describe('CrawlerLogic', () => { timeoutId, }); - CrawlerLogic.actions.createNewTimeoutForCrawlRequests(2000); + CrawlerLogic.actions.createNewTimeoutForCrawlerData(2000); expect(clearTimeout).toHaveBeenCalledWith(timeoutId); }); }); - - describe('getLatestCrawlRequests', () => { - describe('on success', () => { - [ - CrawlerStatus.Pending, - CrawlerStatus.Starting, - CrawlerStatus.Running, - CrawlerStatus.Canceling, - ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { - jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlRequests'); - http.get.mockReturnValueOnce(Promise.resolve([{ status }])); - - CrawlerLogic.actions.getLatestCrawlRequests(); - await nextTick(); - - expect(CrawlerLogic.actions.createNewTimeoutForCrawlRequests).toHaveBeenCalled(); - }); - }); - - [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].forEach((status) => { - it(`clears the timeout and fetches data for status ${status}`, async () => { - jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); - jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); - http.get.mockReturnValueOnce(Promise.resolve([{ status }])); - - CrawlerLogic.actions.getLatestCrawlRequests(); - await nextTick(); - - expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); - expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); - }); - - it(`optionally supresses fetching data for status ${status}`, async () => { - jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); - jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); - http.get.mockReturnValueOnce(Promise.resolve([{ status }])); - - CrawlerLogic.actions.getLatestCrawlRequests(false); - await nextTick(); - - expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); - expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalledTimes(0); - }); - }); - }); - - describe('on failure', () => { - it('creates a new timeout', async () => { - jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlRequests'); - http.get.mockReturnValueOnce(Promise.reject()); - - CrawlerLogic.actions.getLatestCrawlRequests(); - await nextTick(); - - expect(CrawlerLogic.actions.createNewTimeoutForCrawlRequests).toHaveBeenCalled(); - }); - }); - }); }); describe('selectors', () => { describe('mostRecentCrawlRequestStatus', () => { - it('is Success when there are no crawl requests', () => { + it('is Success when there is no recent crawl request', () => { mount({ - crawlRequests: [], + mostRecentCrawlRequest: null, }); expect(CrawlerLogic.values.mostRecentCrawlRequestStatus).toEqual(CrawlerStatus.Success); }); - it('is Success when there are only crawl requests', () => { + it('is the most recent crawl request status', () => { mount({ - crawlRequests: [ - { - id: '2', - status: CrawlerStatus.Skipped, - createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - { - id: '1', - status: CrawlerStatus.Skipped, - createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - ], - }); - - expect(CrawlerLogic.values.mostRecentCrawlRequestStatus).toEqual(CrawlerStatus.Success); - }); - - it('is the first non-skipped crawl request status', () => { - mount({ - crawlRequests: [ - { - id: '3', - status: CrawlerStatus.Skipped, - createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - { - id: '2', - status: CrawlerStatus.Failed, - createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - { - id: '1', - status: CrawlerStatus.Success, - createdAt: 'Mon, 29 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - ], + mostRecentCrawlRequest: { + id: '2', + status: CrawlerStatus.Failed, + createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, }); expect(CrawlerLogic.values.mostRecentCrawlRequestStatus).toEqual(CrawlerStatus.Failed); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 972532597e344..5b9960ddf54e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -12,34 +12,33 @@ import { flashAPIErrors } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; -import { - CrawlerData, - CrawlerDomain, - CrawlRequest, - CrawlRequestFromServer, - CrawlerStatus, -} from './types'; -import { crawlerDataServerToClient, crawlRequestServerToClient } from './utils'; +import { CrawlerData, CrawlerDomain, CrawlEvent, CrawlRequest, CrawlerStatus } from './types'; +import { crawlerDataServerToClient } from './utils'; const POLLING_DURATION = 1000; const POLLING_DURATION_ON_FAILURE = 5000; +const ACTIVE_STATUSES = [ + CrawlerStatus.Pending, + CrawlerStatus.Starting, + CrawlerStatus.Running, + CrawlerStatus.Canceling, +]; export interface CrawlerValues { - crawlRequests: CrawlRequest[]; + events: CrawlEvent[]; dataLoading: boolean; domains: CrawlerDomain[]; - mostRecentCrawlRequestStatus: CrawlerStatus; + mostRecentCrawlRequest: CrawlRequest | null; + mostRecentCrawlRequestStatus: CrawlerStatus | null; timeoutId: NodeJS.Timeout | null; } interface CrawlerActions { clearTimeoutId(): void; - createNewTimeoutForCrawlRequests(duration: number): { duration: number }; + createNewTimeoutForCrawlerData(duration: number): { duration: number }; fetchCrawlerData(): void; - getLatestCrawlRequests(refreshData?: boolean): { refreshData?: boolean }; onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - onReceiveCrawlRequests(crawlRequests: CrawlRequest[]): { crawlRequests: CrawlRequest[] }; startCrawl(): void; stopCrawl(): void; } @@ -48,12 +47,10 @@ export const CrawlerLogic = kea>({ path: ['enterprise_search', 'app_search', 'crawler_logic'], actions: { clearTimeoutId: true, - createNewTimeoutForCrawlRequests: (duration) => ({ duration }), + createNewTimeoutForCrawlerData: (duration) => ({ duration }), fetchCrawlerData: true, - getLatestCrawlRequests: (refreshData) => ({ refreshData }), onCreateNewTimeout: (timeoutId) => ({ timeoutId }), onReceiveCrawlerData: (data) => ({ data }), - onReceiveCrawlRequests: (crawlRequests) => ({ crawlRequests }), startCrawl: () => null, stopCrawl: () => null, }, @@ -70,10 +67,16 @@ export const CrawlerLogic = kea>({ onReceiveCrawlerData: (_, { data: { domains } }) => domains, }, ], - crawlRequests: [ + events: [ [], { - onReceiveCrawlRequests: (_, { crawlRequests }) => crawlRequests, + onReceiveCrawlerData: (_, { data: { events } }) => events, + }, + ], + mostRecentCrawlRequest: [ + null, + { + onReceiveCrawlerData: (_, { data: { mostRecentCrawlRequest } }) => mostRecentCrawlRequest, }, ], timeoutId: [ @@ -86,15 +89,12 @@ export const CrawlerLogic = kea>({ }, selectors: ({ selectors }) => ({ mostRecentCrawlRequestStatus: [ - () => [selectors.crawlRequests], - (crawlRequests: CrawlerValues['crawlRequests']) => { - const eligibleCrawlRequests = crawlRequests.filter( - (req) => req.status !== CrawlerStatus.Skipped - ); - if (eligibleCrawlRequests.length === 0) { - return CrawlerStatus.Success; + () => [selectors.mostRecentCrawlRequest], + (crawlRequest: CrawlerValues['mostRecentCrawlRequest']) => { + if (crawlRequest) { + return crawlRequest.status; } - return eligibleCrawlRequests[0].status; + return CrawlerStatus.Success; }, ], }), @@ -107,10 +107,21 @@ export const CrawlerLogic = kea>({ const response = await http.get(`/internal/app_search/engines/${engineName}/crawler`); const crawlerData = crawlerDataServerToClient(response); - actions.onReceiveCrawlerData(crawlerData); + + const continuePoll = + (crawlerData.mostRecentCrawlRequest && + ACTIVE_STATUSES.includes(crawlerData.mostRecentCrawlRequest.status)) || + crawlerData.events.find((event) => ACTIVE_STATUSES.includes(event.status)); + + if (continuePoll) { + actions.createNewTimeoutForCrawlerData(POLLING_DURATION); + } else { + actions.clearTimeoutId(); + } } catch (e) { flashAPIErrors(e); + actions.createNewTimeoutForCrawlerData(POLLING_DURATION_ON_FAILURE); } }, startCrawl: async () => { @@ -119,7 +130,7 @@ export const CrawlerLogic = kea>({ try { await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`); - actions.getLatestCrawlRequests(); + actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); } @@ -130,55 +141,22 @@ export const CrawlerLogic = kea>({ try { await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests/cancel`); - actions.getLatestCrawlRequests(); + actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); } }, - createNewTimeoutForCrawlRequests: ({ duration }) => { + createNewTimeoutForCrawlerData: ({ duration }) => { if (values.timeoutId) { clearTimeout(values.timeoutId); } const timeoutIdId = setTimeout(() => { - actions.getLatestCrawlRequests(); + actions.fetchCrawlerData(); }, duration); actions.onCreateNewTimeout(timeoutIdId); }, - getLatestCrawlRequests: async ({ refreshData = true }) => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - const crawlRequestsFromServer: CrawlRequestFromServer[] = await http.get( - `/internal/app_search/engines/${engineName}/crawler/crawl_requests` - ); - const crawlRequests = crawlRequestsFromServer.map(crawlRequestServerToClient); - actions.onReceiveCrawlRequests(crawlRequests); - if ( - [ - CrawlerStatus.Pending, - CrawlerStatus.Starting, - CrawlerStatus.Running, - CrawlerStatus.Canceling, - ].includes(crawlRequests[0]?.status) - ) { - actions.createNewTimeoutForCrawlRequests(POLLING_DURATION); - } else if ( - [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].includes( - crawlRequests[0]?.status - ) - ) { - actions.clearTimeoutId(); - if (refreshData) { - actions.fetchCrawlerData(); - } - } - } catch (e) { - actions.createNewTimeoutForCrawlRequests(POLLING_DURATION_ON_FAILURE); - } - }, }), events: ({ values }) => ({ beforeUnmount: () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 705dfc44baa88..67f8826dace8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -28,7 +28,7 @@ import { CrawlerPolicies, CrawlerRules, CrawlerStatus, - CrawlRequestFromServer, + CrawlEventFromServer, } from './types'; const domains: CrawlerDomainFromServer[] = [ @@ -65,9 +65,10 @@ const domains: CrawlerDomainFromServer[] = [ }, ]; -const crawlRequests: CrawlRequestFromServer[] = [ +const events: CrawlEventFromServer[] = [ { id: 'a', + stage: 'crawl', status: CrawlerStatus.Canceled, created_at: 'Mon, 31 Aug 2020 11:00:00 +0000', began_at: 'Mon, 31 Aug 2020 12:00:00 +0000', @@ -75,6 +76,7 @@ const crawlRequests: CrawlRequestFromServer[] = [ }, { id: 'b', + stage: 'crawl', status: CrawlerStatus.Success, created_at: 'Mon, 31 Aug 2020 14:00:00 +0000', began_at: 'Mon, 31 Aug 2020 15:00:00 +0000', @@ -86,7 +88,8 @@ describe('CrawlerOverview', () => { const mockValues = { dataLoading: false, domains, - crawlRequests, + events, + mostRecentCrawlRequest: null, }; beforeEach(() => { @@ -118,7 +121,7 @@ describe('CrawlerOverview', () => { }); it('hides the domain and crawl request tables when there are no domains, and no crawl requests', () => { - setMockValues({ ...mockValues, domains: [], crawlRequests: [] }); + setMockValues({ ...mockValues, domains: [], events: [] }); const wrapper = shallow(); @@ -130,7 +133,7 @@ describe('CrawlerOverview', () => { }); it('shows the domain and the crawl request tables when there are domains, but no crawl requests', () => { - setMockValues({ ...mockValues, crawlRequests: [] }); + setMockValues({ ...mockValues, events: [] }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index be4e1743748b7..b6fa50e06c904 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -29,7 +29,7 @@ import { CRAWLER_TITLE } from './constants'; import { CrawlerLogic } from './crawler_logic'; export const CrawlerOverview: React.FC = () => { - const { crawlRequests, dataLoading, domains } = useValues(CrawlerLogic); + const { events, dataLoading, domains } = useValues(CrawlerLogic); return ( { )} - {(crawlRequests.length > 0 || domains.length > 0) && ( + {(events.length > 0 || domains.length > 0) && ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts index 9a5d99abdd469..a701c43d4775c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts @@ -43,6 +43,8 @@ const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { available_deduplication_fields: ['title', 'description'], }, ], + events: [], + most_recent_crawl_request: null, }; const MOCK_CLIENT_CRAWLER_DATA = crawlerDataServerToClient(MOCK_SERVER_CRAWLER_DATA); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx index 8c49e97d6462b..da2b3cf2261b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx @@ -19,7 +19,6 @@ import { CrawlerSingleDomain } from './crawler_single_domain'; describe('CrawlerRouter', () => { const mockActions = { fetchCrawlerData: jest.fn(), - getLatestCrawlRequests: jest.fn(), }; let wrapper: ShallowWrapper; @@ -32,7 +31,6 @@ describe('CrawlerRouter', () => { it('calls fetchCrawlerData and starts polling on page load', () => { expect(mockActions.fetchCrawlerData).toHaveBeenCalledTimes(1); - expect(mockActions.getLatestCrawlRequests).toHaveBeenCalledWith(false); }); it('renders a crawler views', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx index f95423cd2c704..2cebb28d962f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx @@ -18,11 +18,10 @@ import { CrawlerOverview } from './crawler_overview'; import { CrawlerSingleDomain } from './crawler_single_domain'; export const CrawlerRouter: React.FC = () => { - const { fetchCrawlerData, getLatestCrawlRequests } = useActions(CrawlerLogic); + const { fetchCrawlerData } = useActions(CrawlerLogic); useEffect(() => { fetchCrawlerData(); - getLatestCrawlRequests(false); }, []); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index 76612ee913c48..beb1e65af47a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -36,7 +36,6 @@ const MOCK_VALUES = { const MOCK_ACTIONS = { fetchCrawlerData: jest.fn(), fetchDomainData: jest.fn(), - getLatestCrawlRequests: jest.fn(), }; describe('CrawlerSingleDomain', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 8cfbce6c10315..a4d5a984faaec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -120,10 +120,14 @@ export interface CrawlerDomainFromServer { export interface CrawlerData { domains: CrawlerDomain[]; + events: CrawlEvent[]; + mostRecentCrawlRequest: CrawlRequest | null; } export interface CrawlerDataFromServer { domains: CrawlerDomainFromServer[]; + events: CrawlEventFromServer[]; + most_recent_crawl_request: CrawlRequestFromServer | null; } export interface CrawlerDomainValidationResultFromServer { @@ -191,6 +195,26 @@ export interface CrawlRequest { completedAt: string | null; } +export type CrawlEventStage = 'crawl' | 'process'; + +export interface CrawlEventFromServer { + id: string; + stage: CrawlEventStage; + status: CrawlerStatus; + created_at: string; + began_at: string | null; + completed_at: string | null; +} + +export interface CrawlEvent { + id: string; + stage: CrawlEventStage; + status: CrawlerStatus; + createdAt: string; + beganAt: string | null; + completedAt: string | null; +} + export const readableCrawlerStatuses: { [key in CrawlerStatus]: string } = { [CrawlerStatus.Pending]: i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusOptions.pending', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts index b679a7cc9c12c..fc810ba8fd7cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -153,10 +153,27 @@ describe('crawlerDataServerToClient', () => { beforeAll(() => { output = crawlerDataServerToClient({ domains, + events: [ + { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + stage: 'crawl', + created_at: 'Mon, 31 Aug 2020 17:00:00 +0000', + began_at: null, + completed_at: null, + }, + ], + most_recent_crawl_request: { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + created_at: 'Mon, 31 Aug 2020 17:00:00 +0000', + began_at: null, + completed_at: null, + }, }); }); - it('converts all domains from the server form to their client form', () => { + it('converts all data from the server form to their client form', () => { expect(output.domains).toEqual([ { id: 'x', @@ -185,6 +202,23 @@ describe('crawlerDataServerToClient', () => { availableDeduplicationFields: ['title', 'description'], }, ]); + expect(output.events).toEqual([ + { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + stage: 'crawl', + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + ]); + expect(output.mostRecentCrawlRequest).toEqual({ + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts index e44e6c0e652fa..9c94040355d47 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -16,6 +16,8 @@ import { CrawlerDomainValidationStep, CrawlRequestFromServer, CrawlRequest, + CrawlEventFromServer, + CrawlEvent, } from './types'; export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): CrawlerDomain { @@ -76,11 +78,34 @@ export function crawlRequestServerToClient(crawlRequest: CrawlRequestFromServer) }; } +export function crawlerEventServerToClient(event: CrawlEventFromServer): CrawlEvent { + const { + id, + stage, + status, + created_at: createdAt, + began_at: beganAt, + completed_at: completedAt, + } = event; + + return { + id, + stage, + status, + createdAt, + beganAt, + completedAt, + }; +} + export function crawlerDataServerToClient(payload: CrawlerDataFromServer): CrawlerData { - const { domains } = payload; + const { domains, events, most_recent_crawl_request: mostRecentCrawlRequest } = payload; return { domains: domains.map((domain) => crawlerDomainServerToClient(domain)), + events: events.map((event) => crawlerEventServerToClient(event)), + mostRecentCrawlRequest: + mostRecentCrawlRequest && crawlRequestServerToClient(mostRecentCrawlRequest), }; } From 22f0e762edf24493cde408b46617c43db7fa2752 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 14 Sep 2021 07:03:34 -0400 Subject: [PATCH 09/39] Upgrade set-value to 4.1.0 (#111988) --- package.json | 2 +- yarn.lock | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 06755da56451a..3a910de84a2c7 100644 --- a/package.json +++ b/package.json @@ -375,7 +375,7 @@ "safe-squel": "^5.12.5", "seedrandom": "^3.0.5", "semver": "^7.3.2", - "set-value": "^3.0.2", + "set-value": "^4.1.0", "source-map-support": "^0.5.19", "stats-lite": "^2.2.0", "strip-ansi": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 9b82ebac25676..f895c607e572d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16682,6 +16682,11 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-primitive@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-3.0.1.tgz#98c4db1abff185485a657fc2905052b940524d05" + integrity sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w== + is-promise@^2.1, is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -25061,12 +25066,13 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -set-value@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== +set-value@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09" + integrity sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw== dependencies: is-plain-object "^2.0.4" + is-primitive "^3.0.1" setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" From 6e7e5155f596502c3c2e8ca369835000419169d5 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 14 Sep 2021 14:38:51 +0300 Subject: [PATCH 10/39] [TSVB] Doesn't work correctly with pipeline aggregations in "entire time range" mode (#105073) * Use date_histogram instead of auto_date_histogram in pipeline aggregations * Fix ci * Fix eslint * start disable parent pipeline aggs and show error * Fix CI * Fix eslint * Fix CI * Add functional tests * Some fixes * Fix lint * Use agg_utils * Fix lint * Fix text * Fix lint * Fix tests * Fixed condition * Fix math aggregation * math should pass panel type as prop Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_type_timeseries/common/errors.ts | 58 ++++++++++++++++ .../common/fields_utils.ts | 21 +----- .../common/validate_interval.ts | 21 +----- .../application/components/aggs/agg.tsx | 21 ++++-- .../components/aggs/agg_select.tsx | 28 +++++++- .../{unsupported_agg.tsx => invalid_agg.tsx} | 21 +++--- .../application/components/aggs/math.js | 1 + .../aggs/temporary_unsupported_agg.tsx | 46 ------------- .../default_search_capabilities.test.ts | 2 + .../default_search_capabilities.ts | 33 +++++++++ .../rollup_search_capabilities.test.ts | 3 +- .../search_strategies_registry.test.ts | 4 +- .../default_search_strategy.test.ts | 9 ++- .../strategies/default_search_strategy.ts | 1 + .../strategies/rollup_search_strategy.ts | 1 + .../server/lib/vis_data/get_series_data.ts | 12 +++- .../server/lib/vis_data/get_table_data.ts | 8 +++ .../server/lib/vis_data/helpers/check_aggs.ts | 27 ++++++++ .../server/lib/vis_data/helpers/index.ts | 1 + .../series/date_histogram.js | 47 +++++++++---- .../series/date_histogram.test.js | 2 +- .../table/date_histogram.ts | 42 ++++++++--- test/functional/apps/visualize/_tsvb_chart.ts | 69 +++++++++++++++++++ .../page_objects/visual_builder_page.ts | 10 +++ .../translations/translations/ja-JP.json | 4 -- .../translations/translations/zh-CN.json | 4 -- 26 files changed, 352 insertions(+), 144 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/common/errors.ts rename src/plugins/vis_type_timeseries/public/application/components/aggs/{unsupported_agg.tsx => invalid_agg.tsx} (62%) delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/check_aggs.ts diff --git a/src/plugins/vis_type_timeseries/common/errors.ts b/src/plugins/vis_type_timeseries/common/errors.ts new file mode 100644 index 0000000000000..6a23a003d29ee --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/errors.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable max-classes-per-file */ + +import { i18n } from '@kbn/i18n'; + +export class UIError extends Error { + constructor(message: string) { + super(message); + } + + public get name() { + return this.constructor.name; + } + + public get errBody() { + return this.message; + } +} + +export class FieldNotFoundError extends UIError { + constructor(name: string) { + super( + i18n.translate('visTypeTimeseries.errors.fieldNotFound', { + defaultMessage: `Field "{field}" not found`, + values: { field: name }, + }) + ); + } +} + +export class ValidateIntervalError extends UIError { + constructor() { + super( + i18n.translate('visTypeTimeseries.errors.maxBucketsExceededErrorMessage', { + defaultMessage: + 'Your query attempted to fetch too much data. Reducing the time range or changing the interval used usually fixes the issue.', + }) + ); + } +} + +export class AggNotSupportedInMode extends UIError { + constructor(metricType: string, timeRangeMode: string) { + super( + i18n.translate('visTypeTimeseries.wrongAggregationErrorMessage', { + defaultMessage: 'The aggregation {metricType} is not supported in {timeRangeMode} mode', + values: { metricType, timeRangeMode }, + }) + ); + } +} diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.ts b/src/plugins/vis_type_timeseries/common/fields_utils.ts index b64fcc383a1bb..1af0340dfa525 100644 --- a/src/plugins/vis_type_timeseries/common/fields_utils.ts +++ b/src/plugins/vis_type_timeseries/common/fields_utils.ts @@ -6,29 +6,10 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { FieldSpec } from '../../data/common'; import { isNestedField } from '../../data/common'; import { FetchedIndexPattern, SanitizedFieldType } from './types'; - -export class FieldNotFoundError extends Error { - constructor(name: string) { - super( - i18n.translate('visTypeTimeseries.fields.fieldNotFound', { - defaultMessage: `Field "{field}" not found`, - values: { field: name }, - }) - ); - } - - public get name() { - return this.constructor.name; - } - - public get errBody() { - return this.message; - } -} +import { FieldNotFoundError } from './errors'; export const extractFieldLabel = ( fields: SanitizedFieldType[], diff --git a/src/plugins/vis_type_timeseries/common/validate_interval.ts b/src/plugins/vis_type_timeseries/common/validate_interval.ts index 7f9ccf20c0eb1..7c7a4e7badfc0 100644 --- a/src/plugins/vis_type_timeseries/common/validate_interval.ts +++ b/src/plugins/vis_type_timeseries/common/validate_interval.ts @@ -6,28 +6,9 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { GTE_INTERVAL_RE } from './interval_regexp'; import { parseInterval, TimeRangeBounds } from '../../data/common'; - -export class ValidateIntervalError extends Error { - constructor() { - super( - i18n.translate('visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage', { - defaultMessage: - 'Your query attempted to fetch too much data. Reducing the time range or changing the interval used usually fixes the issue.', - }) - ); - } - - public get name() { - return this.constructor.name; - } - - public get errBody() { - return this.message; - } -} +import { ValidateIntervalError } from './errors'; export function validateInterval(bounds: TimeRangeBounds, interval: string, maxBuckets: number) { const { min, max } = bounds; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index 3c68cb02dd07e..08f8c072eef3b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -7,16 +7,17 @@ */ import React, { useMemo, useEffect, HTMLAttributes } from 'react'; +import { EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; // @ts-ignore import { aggToComponent } from '../lib/agg_to_component'; // @ts-ignore import { isMetricEnabled } from '../../lib/check_ui_restrictions'; +import { getInvalidAggComponent } from './invalid_agg'; // @ts-expect-error not typed yet import { seriesChangeHandler } from '../lib/series_change_handler'; import { checkIfNumericMetric } from '../lib/check_if_numeric_metric'; import { getFormatterType } from '../lib/get_formatter_type'; -import { UnsupportedAgg } from './unsupported_agg'; -import { TemporaryUnsupportedAgg } from './temporary_unsupported_agg'; import { DATA_FORMATTERS } from '../../../../common/enums'; import type { Metric, Panel, Series, SanitizedFieldType } from '../../../../common/types'; import type { DragHandleProps } from '../../../types'; @@ -43,9 +44,21 @@ export function Agg(props: AggProps) { let Component = aggToComponent[model.type]; if (!Component) { - Component = UnsupportedAgg; + Component = getInvalidAggComponent( + {props.model.type} }} + /> + ); } else if (!isMetricEnabled(model.type, uiRestrictions)) { - Component = TemporaryUnsupportedAgg; + Component = getInvalidAggComponent( + {props.model.type} }} + /> + ); } const style = { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx index 719ebbbe5a91d..2959712bb9f00 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; // @ts-ignore @@ -16,6 +16,8 @@ import { getAggsByType, getAggsByPredicate } from '../../../../common/agg_utils' import type { Agg } from '../../../../common/agg_utils'; import type { Metric } from '../../../../common/types'; import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; +import { PanelModelContext } from '../../contexts/panel_model_context'; +import { PANEL_TYPES, TIME_RANGE_DATA_MODES } from '../../../../common/enums'; type AggSelectOption = EuiComboBoxOptionOption; @@ -35,16 +37,35 @@ function filterByPanelType(panelType: string) { panelType === 'table' ? agg.value !== TSVB_METRIC_TYPES.SERIES_AGG : true; } +export function isMetricAvailableForPanel( + aggId: string, + panelType: string, + timeRangeMode?: string +) { + if ( + panelType !== PANEL_TYPES.TIMESERIES && + timeRangeMode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE + ) { + return ( + !pipelineAggs.some((agg) => agg.value === aggId) && aggId !== TSVB_METRIC_TYPES.SERIES_AGG + ); + } + + return true; +} + interface AggSelectUiProps { id: string; panelType: string; siblings: Metric[]; value: string; uiRestrictions?: TimeseriesUIRestrictions; + timeRangeMode?: string; onChange: (currentlySelectedOptions: AggSelectOption[]) => void; } export function AggSelect(props: AggSelectUiProps) { + const panelModel = useContext(PanelModelContext); const { siblings, panelType, value, onChange, uiRestrictions, ...rest } = props; const selectedOptions = allAggOptions.filter((option) => { @@ -69,7 +90,10 @@ export function AggSelect(props: AggSelectUiProps) { } else { const disableSiblingAggs = (agg: AggSelectOption) => ({ ...agg, - disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions), + disabled: + !enablePipelines || + !isMetricEnabled(agg.value, uiRestrictions) || + !isMetricAvailableForPanel(agg.value as string, panelType, panelModel?.time_range_mode), }); options = [ diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/invalid_agg.tsx similarity index 62% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.tsx rename to src/plugins/vis_type_timeseries/public/application/components/aggs/invalid_agg.tsx index 70c5499597e66..7fb4b31c2347e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/invalid_agg.tsx @@ -7,13 +7,12 @@ */ import React from 'react'; -import { EuiCode, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle } from '@elastic/eui'; import { AggRow } from './agg_row'; import type { Metric } from '../../../../common/types'; import { DragHandleProps } from '../../../types'; -interface UnsupportedAggProps { +interface InvalidAggProps { disableDelete: boolean; model: Metric; siblings: Metric[]; @@ -22,7 +21,9 @@ interface UnsupportedAggProps { onDelete: () => void; } -export function UnsupportedAgg(props: UnsupportedAggProps) { +export const getInvalidAggComponent = (message: JSX.Element | string) => ( + props: InvalidAggProps +) => { return ( - - - {props.model.type} }} - /> - + + {message} ); -} +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js index e92659e677860..f00a485f2d759 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js @@ -69,6 +69,7 @@ export function MathAgg(props) { id={htmlId('aggregation')} siblings={props.siblings} value={model.type} + panelType={props.panel.type} onChange={handleSelectChange('type')} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx deleted file mode 100644 index b85da5955ac65..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx +++ /dev/null @@ -1,46 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiCode, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AggRow } from './agg_row'; -import type { Metric } from '../../../../common/types'; -import { DragHandleProps } from '../../../types'; - -interface TemporaryUnsupportedAggProps { - disableDelete: boolean; - model: Metric; - siblings: Metric[]; - dragHandleProps: DragHandleProps; - onAdd: () => void; - onDelete: () => void; -} - -export function TemporaryUnsupportedAgg(props: TemporaryUnsupportedAggProps) { - return ( - - - - {props.model.type} }} - /> - - - - ); -} diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts index 8029e8684c441..351691d4c42a3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts @@ -7,12 +7,14 @@ */ import { DefaultSearchCapabilities } from './default_search_capabilities'; +import type { Panel } from '../../../../common/types'; describe('DefaultSearchCapabilities', () => { let defaultSearchCapabilities: DefaultSearchCapabilities; beforeEach(() => { defaultSearchCapabilities = new DefaultSearchCapabilities({ + panel: {} as Panel, timezone: 'UTC', maxBucketsLimit: 2000, }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts index b60d2e61e9a43..0240ac93b60e8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts @@ -13,19 +13,30 @@ import { getSuitableUnit, } from '../../vis_data/helpers/unit_to_seconds'; import { RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions'; +import { + TIME_RANGE_DATA_MODES, + PANEL_TYPES, + BUCKET_TYPES, + TSVB_METRIC_TYPES, +} from '../../../../common/enums'; +import { getAggsByType, AGG_TYPE } from '../../../../common/agg_utils'; +import type { Panel } from '../../../../common/types'; export interface SearchCapabilitiesOptions { timezone?: string; maxBucketsLimit: number; + panel?: Panel; } export class DefaultSearchCapabilities { public timezone: SearchCapabilitiesOptions['timezone']; public maxBucketsLimit: SearchCapabilitiesOptions['maxBucketsLimit']; + public panel?: Panel; constructor(options: SearchCapabilitiesOptions) { this.timezone = options.timezone; this.maxBucketsLimit = options.maxBucketsLimit; + this.panel = options.panel; } public get defaultTimeInterval() { @@ -33,6 +44,28 @@ export class DefaultSearchCapabilities { } public get whiteListedMetrics() { + if ( + this.panel && + this.panel.type !== PANEL_TYPES.TIMESERIES && + this.panel.time_range_mode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE + ) { + const aggs = getAggsByType((agg) => agg.id); + const allAvailableAggs = [ + ...aggs[AGG_TYPE.METRIC], + ...aggs[AGG_TYPE.SIBLING_PIPELINE], + TSVB_METRIC_TYPES.MATH, + BUCKET_TYPES.TERMS, + ].reduce( + (availableAggs, aggType) => ({ + ...availableAggs, + [aggType]: { + '*': true, + }, + }), + {} + ); + return this.createUiRestriction(allAvailableAggs); + } return this.createUiRestriction(); } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts index 7426c74dc2426..e1cc1f1f26eb2 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts @@ -7,6 +7,7 @@ */ import { Unit } from '@elastic/datemath'; +import type { Panel } from '../../../../common/types'; import { RollupSearchCapabilities } from './rollup_search_capabilities'; describe('Rollup Search Capabilities', () => { @@ -32,7 +33,7 @@ describe('Rollup Search Capabilities', () => { }; rollupSearchCaps = new RollupSearchCapabilities( - { maxBucketsLimit: 2000, timezone: 'UTC' }, + { maxBucketsLimit: 2000, timezone: 'UTC', panel: {} as Panel }, fieldsCapabilities, rollupIndex ); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index f6114a4117bb8..7f5f12602998f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -52,7 +52,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a DefaultSearchStrategy instance', async () => { - const req = { body: {} } as VisTypeTimeseriesRequest; + const req = { body: { panels: [] } } as VisTypeTimeseriesRequest; const { searchStrategy, capabilities } = (await registry.getViableStrategy( requestContext, @@ -73,7 +73,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a MockSearchStrategy instance', async () => { - const req = { body: {} } as VisTypeTimeseriesRequest; + const req = { body: { panels: [] } } as VisTypeTimeseriesRequest; const anotherSearchStrategy = new MockSearchStrategy(); registry.addStrategy(anotherSearchStrategy); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts index 9fa79c7b80f8c..3638a438ec736 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts @@ -27,9 +27,11 @@ describe('DefaultSearchStrategy', () => { let req: VisTypeTimeseriesVisDataRequest; beforeEach(() => { - req = { - body: {}, - } as VisTypeTimeseriesVisDataRequest; + req = ({ + body: { + panels: [], + }, + } as unknown) as VisTypeTimeseriesVisDataRequest; defaultSearchStrategy = new DefaultSearchStrategy(); }); @@ -46,6 +48,7 @@ describe('DefaultSearchStrategy', () => { expect(value.capabilities).toMatchInlineSnapshot(` DefaultSearchCapabilities { "maxBucketsLimit": undefined, + "panel": undefined, "timezone": undefined, } `); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index 17451f7e5777e..34892ec797c0b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -27,6 +27,7 @@ export class DefaultSearchStrategy extends AbstractSearchStrategy { return { isViable: true, capabilities: new DefaultSearchCapabilities({ + panel: req.body.panels ? req.body.panels[0] : null, timezone: req.body.timerange?.timezone, maxBucketsLimit: await uiSettings.get(MAX_BUCKETS_SETTING), }), diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts index 0ac00863d0a73..7a1d1574aa7bb 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts @@ -74,6 +74,7 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { capabilities = new RollupSearchCapabilities( { maxBucketsLimit: await uiSettings.get(MAX_BUCKETS_SETTING), + panel: req.body.panels ? req.body.panels[0] : null, }, fieldsCapabilities, rollupIndex diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts index 12fe95ccc50ca..a9a3825f5a9df 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts @@ -13,6 +13,8 @@ import { getAnnotations } from './get_annotations'; import { handleResponseBody } from './series/handle_response_body'; import { getSeriesRequestParams } from './series/get_request_params'; import { getActiveSeries } from './helpers/get_active_series'; +import { isAggSupported } from './helpers/check_aggs'; +import { isEntireTimeRangeMode } from './helpers/get_timerange_mode'; import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, @@ -55,9 +57,13 @@ export async function getSeriesData( const handleError = handleErrorResponse(panel); try { - const bodiesPromises = getActiveSeries(panel).map((series) => - getSeriesRequestParams(req, panel, panelIndex, series, capabilities, services) - ); + const bodiesPromises = getActiveSeries(panel).map((series) => { + if (isEntireTimeRangeMode(panel, series)) { + isAggSupported(series.metrics); + } + + return getSeriesRequestParams(req, panel, panelIndex, series, capabilities, services); + }); const fieldFetchServices = { indexPatternsService, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts index 7e1332f801856..3b53147dc6f93 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts @@ -15,6 +15,8 @@ import { processBucket } from './table/process_bucket'; import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher'; import { extractFieldLabel } from '../../../common/fields_utils'; +import { isAggSupported } from './helpers/check_aggs'; +import { isEntireTimeRangeMode } from './helpers/get_timerange_mode'; import type { VisTypeTimeseriesRequestHandlerContext, @@ -71,6 +73,12 @@ export async function getTableData( const handleError = handleErrorResponse(panel); try { + if (isEntireTimeRangeMode(panel)) { + panel.series.forEach((column) => { + isAggSupported(column.metrics); + }); + } + const body = await buildTableRequest({ req, panel, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/check_aggs.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/check_aggs.ts new file mode 100644 index 0000000000000..bc420045dd434 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/check_aggs.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AggNotSupportedInMode } from '../../../../common/errors'; +import { getAggsByType, AGG_TYPE } from '../../../../common/agg_utils'; +import { TSVB_METRIC_TYPES, TIME_RANGE_DATA_MODES } from '../../../../common/enums'; +import { Metric } from '../../../../common/types'; + +export function isAggSupported(metrics: Metric[]) { + const parentPipelineAggs = getAggsByType((agg) => agg.id)[AGG_TYPE.PARENT_PIPELINE]; + const metricTypes = metrics.filter( + (metric) => + parentPipelineAggs.includes(metric.type) || metric.type === TSVB_METRIC_TYPES.SERIES_AGG + ); + + if (metricTypes.length) { + throw new AggNotSupportedInMode( + metricTypes.map((metric) => metric.type).join(', '), + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE + ); + } +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.ts index 41d302422c0b7..7dfecc9811dd9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.ts @@ -15,6 +15,7 @@ export { getBucketsPath } from './get_buckets_path'; export { isEntireTimeRangeMode, isLastValueTimerangeMode } from './get_timerange_mode'; export { getLastMetric } from './get_last_metric'; export { getSplits } from './get_splits'; +export { isAggSupported } from './check_aggs'; // @ts-expect-error no typed yet export { bucketTransform } from './bucket_transform'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index cec3e82d5e37c..6349a75993aa8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -11,6 +11,8 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { offsetTime } from '../../offset_time'; import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; +import { AGG_TYPE, getAggsByType } from '../../../../../common/agg_utils'; +import { TSVB_METRIC_TYPES } from '../../../../../common/enums'; const { dateHistogramInterval } = search.aggs; @@ -30,19 +32,17 @@ export function dateHistogram( const { timeField, interval, maxBars } = await buildSeriesMetaParams(); const { from, to } = offsetTime(req, series.offset_time); + const { timezone } = capabilities; + const { intervalString } = getBucketSize( + req, + interval, + capabilities, + maxBars ? Math.min(maxBarsUiSettings, maxBars) : barTargetUiSettings + ); let bucketInterval; const overwriteDateHistogramForLastBucketMode = () => { - const { timezone } = capabilities; - - const { intervalString } = getBucketSize( - req, - interval, - capabilities, - maxBars ? Math.min(maxBarsUiSettings, maxBars) : barTargetUiSettings - ); - overwrite(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { field: timeField, min_doc_count: 0, @@ -58,12 +58,35 @@ export function dateHistogram( }; const overwriteDateHistogramForEntireTimerangeMode = () => { - overwrite(doc, `aggs.${series.id}.aggs.timeseries.auto_date_histogram`, { + const metricAggs = getAggsByType((agg) => agg.id)[AGG_TYPE.METRIC]; + + // we should use auto_date_histogram only for metric aggregations and math + if ( + series.metrics.every( + (metric) => metricAggs.includes(metric.type) || metric.type === TSVB_METRIC_TYPES.MATH + ) + ) { + overwrite(doc, `aggs.${series.id}.aggs.timeseries.auto_date_histogram`, { + field: timeField, + buckets: 1, + }); + + bucketInterval = `${to.valueOf() - from.valueOf()}ms`; + return; + } + + overwrite(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { field: timeField, - buckets: 1, + min_doc_count: 0, + time_zone: timezone, + extended_bounds: { + min: from.valueOf(), + max: to.valueOf(), + }, + ...dateHistogramInterval(intervalString), }); - bucketInterval = `${to.valueOf() - from.valueOf()}ms`; + bucketInterval = intervalString; }; isLastValueTimerangeMode(panel, series) diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index 022718ece435d..b09b2c28d77e3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -36,7 +36,7 @@ describe('dateHistogram(req, panel, series)', () => { interval: '10s', id: 'panelId', }; - series = { id: 'test' }; + series = { id: 'test', metrics: [{ type: 'avg' }] }; config = { allowLeadingWildcards: true, queryStringOptions: {}, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts index ac19a266430f3..27470d5868a5c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts @@ -9,6 +9,8 @@ import { overwrite, getBucketSize, isLastValueTimerangeMode, getTimerange } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; +import { AGG_TYPE, getAggsByType } from '../../../../../common/agg_utils'; +import { TSVB_METRIC_TYPES } from '../../../../../common/enums'; import type { TableRequestProcessorsFunction, TableSearchRequestMeta } from './types'; @@ -32,10 +34,10 @@ export const dateHistogram: TableRequestProcessorsFunction = ({ panelId: panel.id, }; - const overwriteDateHistogramForLastBucketMode = () => { - const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); - const { timezone } = capabilities; + const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); + const { timezone } = capabilities; + const overwriteDateHistogramForLastBucketMode = () => { panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); @@ -58,19 +60,41 @@ export const dateHistogram: TableRequestProcessorsFunction = ({ }; const overwriteDateHistogramForEntireTimerangeMode = () => { - const intervalString = `${to.valueOf() - from.valueOf()}ms`; + const metricAggs = getAggsByType((agg) => agg.id)[AGG_TYPE.METRIC]; + let bucketInterval; panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); - overwrite(doc, `${aggRoot}.timeseries.auto_date_histogram`, { - field: timeField, - buckets: 1, - }); + // we should use auto_date_histogram only for metric aggregations and math + if ( + column.metrics.every( + (metric) => metricAggs.includes(metric.type) || metric.type === TSVB_METRIC_TYPES.MATH + ) + ) { + overwrite(doc, `${aggRoot}.timeseries.auto_date_histogram`, { + field: timeField, + buckets: 1, + }); + + bucketInterval = `${to.valueOf() - from.valueOf()}ms`; + } else { + overwrite(doc, `${aggRoot}.timeseries.date_histogram`, { + field: timeField, + min_doc_count: 0, + time_zone: timezone, + extended_bounds: { + min: from.valueOf(), + max: to.valueOf(), + }, + ...dateHistogramInterval(intervalString), + }); + bucketInterval = intervalString; + } overwrite(doc, aggRoot.replace(/\.aggs$/, '.meta'), { ...meta, - intervalString, + intervalString: bucketInterval, }); }); }; diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index c7f228e9aa05c..6a5c062268c25 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -103,6 +103,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(kibanaIndexPatternModeValue).to.eql('32,212,254,720'); }); + it('should show error if we use parent pipeline aggregations in entire time range mode', async () => { + await visualBuilder.selectAggType('Max'); + await visualBuilder.setFieldForAggregation('machine.ram'); + await visualBuilder.createNewAgg(); + await visualBuilder.selectAggType('derivative', 1); + await visualBuilder.setFieldForAggregation('Max of machine.ram', 1); + + const value = await visualBuilder.getMetricValue(); + + expect(value).to.eql('0'); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setMetricsDataTimerangeMode('Entire time range'); + await visualBuilder.clickDataTab('metric'); + await visualBuilder.checkInvalidAggComponentIsPresent(); + const error = await visualBuilder.getVisualizeError(); + + expect(error).to.eql( + 'The aggregation derivative is not supported in entire_time_range mode' + ); + }); + describe('Color rules', () => { beforeEach(async () => { await visualBuilder.selectAggType('Min'); @@ -164,6 +186,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.clickDataTab('gauge'); }); + it('should show error if we use parent pipeline aggregations in entire time range mode', async () => { + await visualBuilder.clickPanelOptions('gauge'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.clickDataTab('gauge'); + await visualBuilder.selectAggType('Max'); + await visualBuilder.setFieldForAggregation('machine.ram'); + await visualBuilder.createNewAgg(); + await visualBuilder.selectAggType('derivative', 1); + await visualBuilder.setFieldForAggregation('Max of machine.ram', 1); + + const value = await visualBuilder.getGaugeCount(); + + expect(value).to.eql('0'); + + await visualBuilder.clickPanelOptions('gauge'); + await visualBuilder.setMetricsDataTimerangeMode('Entire time range'); + await visualBuilder.clickDataTab('gauge'); + await visualBuilder.checkInvalidAggComponentIsPresent(); + const error = await visualBuilder.getVisualizeError(); + + expect(error).to.eql( + 'The aggregation derivative is not supported in entire_time_range mode' + ); + }); + it('should verify gauge label and count display', async () => { await visChart.waitForVisualizationRenderingStabilized(); const gaugeLabel = await visualBuilder.getGaugeLabel(); @@ -296,6 +343,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(secondTopNBarStyle).to.contain('background-color: rgb(128, 224, 138);'); }); + it('should show error if we use parent pipeline aggregations in entire time range mode', async () => { + await visualBuilder.selectAggType('Max'); + await visualBuilder.setFieldForAggregation('machine.ram'); + await visualBuilder.createNewAgg(); + await visualBuilder.selectAggType('derivative', 1); + await visualBuilder.setFieldForAggregation('Max of machine.ram', 1); + + const value = await visualBuilder.getTopNCount(); + + expect(value).to.eql('0'); + + await visualBuilder.clickPanelOptions('topN'); + await visualBuilder.setMetricsDataTimerangeMode('Entire time range'); + await visualBuilder.clickDataTab('topN'); + await visualBuilder.checkInvalidAggComponentIsPresent(); + const error = await visualBuilder.getVisualizeError(); + + expect(error).to.eql( + 'The aggregation derivative is not supported in entire_time_range mode' + ); + }); + describe('Color rules', () => { it('should apply color rules to visualization background and bar', async () => { await visualBuilder.selectAggType('Value Count'); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 81b2e2763eb1d..c324de1231b7d 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -873,4 +873,14 @@ export class VisualBuilderPageObject extends FtrService { const areas = (await this.getChartItems(chartData)) as DebugState['areas']; return areas?.[nth]?.lines.y1.points.map(({ x, y }) => [x, y]); } + + public async getVisualizeError() { + const visError = await this.testSubjects.find(`visualization-error`); + const errorSpans = await visError.findAllByClassName('euiText--extraSmall'); + return await errorSpans[0].getVisibleText(); + } + + public async checkInvalidAggComponentIsPresent() { + await this.testSubjects.existOrFail(`invalid_agg`); + } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 48db2a46460a2..082562b4e9819 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5169,7 +5169,6 @@ "visTypeTimeseries.emptyTextValue": "(空)", "visTypeTimeseries.error.requestForPanelFailedErrorMessage": "このパネルのリクエストに失敗しました", "visTypeTimeseries.fetchFields.loadIndexPatternFieldsErrorMessage": "index_pattern フィールドを読み込めません", - "visTypeTimeseries.fields.fieldNotFound": "フィールド\"{field}\"が見つかりません", "visTypeTimeseries.fieldSelect.fieldIsNotValid": "\"{fieldParameter}\"フィールドは無効であり、現在のインデックスで使用できません。新しいフィールドを選択してください。", "visTypeTimeseries.fieldSelect.selectFieldPlaceholder": "フィールドを選択してください...", "visTypeTimeseries.filterRatio.aggregationLabel": "アグリゲーション", @@ -5562,10 +5561,7 @@ "visTypeTimeseries.units.perMillisecond": "ミリ秒単位", "visTypeTimeseries.units.perMinute": "分単位", "visTypeTimeseries.units.perSecond": "秒単位", - "visTypeTimeseries.unsupportedAgg.aggIsNotSupportedDescription": "{modelType} 集約はサポートされなくなりました。", - "visTypeTimeseries.unsupportedAgg.aggIsTemporaryUnsupportedDescription": "{modelType} 集約は現在サポートされていません。", "visTypeTimeseries.unsupportedSplit.splitIsUnsupportedDescription": "{modelType} での分割はサポートされていません。", - "visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage": "クエリが取得を試みたデータが多すぎます。通常、時間範囲を狭くするか、使用される間隔を変更すると、問題が解決します。", "visTypeTimeseries.vars.variableNameAriaLabel": "変数名", "visTypeTimeseries.vars.variableNamePlaceholder": "変数名", "visTypeTimeseries.visEditorVisualization.applyChangesLabel": "変更を適用", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a4d731cebf0bd..f4bb126eb6d92 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5214,7 +5214,6 @@ "visTypeTimeseries.emptyTextValue": "(空)", "visTypeTimeseries.error.requestForPanelFailedErrorMessage": "对此面板的请求失败", "visTypeTimeseries.fetchFields.loadIndexPatternFieldsErrorMessage": "无法加载 index_pattern 字段", - "visTypeTimeseries.fields.fieldNotFound": "未找到字段“{field}”", "visTypeTimeseries.fieldSelect.fieldIsNotValid": "“{fieldParameter}”字段无效,无法用于当前索引。请选择新字段。", "visTypeTimeseries.fieldSelect.selectFieldPlaceholder": "选择字段......", "visTypeTimeseries.filterRatio.aggregationLabel": "聚合", @@ -5608,10 +5607,7 @@ "visTypeTimeseries.units.perMillisecond": "每毫秒", "visTypeTimeseries.units.perMinute": "每分钟", "visTypeTimeseries.units.perSecond": "每秒", - "visTypeTimeseries.unsupportedAgg.aggIsNotSupportedDescription": "不再支持 {modelType} 聚合。", - "visTypeTimeseries.unsupportedAgg.aggIsTemporaryUnsupportedDescription": "当前不支持 {modelType} 聚合。", "visTypeTimeseries.unsupportedSplit.splitIsUnsupportedDescription": "不支持按 {modelType} 拆分", - "visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage": "您的查询尝试提取过多的数据。缩短时间范围或更改所用的时间间隔通常可解决问题。", "visTypeTimeseries.vars.variableNameAriaLabel": "变量名称", "visTypeTimeseries.vars.variableNamePlaceholder": "变量名称", "visTypeTimeseries.visEditorVisualization.applyChangesLabel": "应用更改", From e37cacbe592131610e98115e87875b8a1d6a5345 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 14 Sep 2021 14:03:49 +0200 Subject: [PATCH 11/39] [Lens] Removing trailing semicolor and scrollbar for empty pie chart visualization (#111971) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/lens/public/pie_visualization/render_function.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index a0a845dc96007..34476f4e3062e 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -243,7 +243,7 @@ export function PieComponent( reportDescription={props.args.description} className="lnsPieExpression__container" > - ; + ); } From 11871593738108838c556d7509dc77d50a2f094b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 14 Sep 2021 08:05:14 -0400 Subject: [PATCH 12/39] Update renovate config for platform security modules (#112074) --- renovate.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index f1f1f14ff4389..602b8bc35f4a1 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -85,8 +85,8 @@ 'xml-crypto', '@types/xml-crypto' ], reviewers: ['team:kibana-security'], - matchBaseBranches: ['master', '7.x'], - labels: ['Team:Security'], + matchBaseBranches: ['master'], + labels: ['Team:Security', 'release_note:skip', 'auto-backport'], enabled: true, }, ], From 4e4afc7503dcadc454056388d07a58b51f922265 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 14 Sep 2021 14:40:06 +0200 Subject: [PATCH 13/39] [Discover/Reporting] Fix export that does not contain relative time filter (#110459) * version 1 of fix: we set the time range on the search source at CSV generation time * updated jest tests and updated API for getSharingData * make time range optional for getSharingData * pivot to updating "getTime" functionality by introducing a new flag * update jest snapshots * update comment * refactored coerceToAbsoluteTime -> coerceRelativeTimeToAbsoluteTime and updated behaviour to be more specific * fix jest test * do not change createFilter API, rather create new createRelativeFilter API, also only use this in one place in discover * update jest tests * update mock * update jest test mock Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/query/timefilter/get_time.test.ts | 88 +++++++++++++++++-- .../data/common/query/timefilter/get_time.ts | 71 +++++++++++---- .../public/query/timefilter/timefilter.ts | 24 ++++- .../timefilter/timefilter_service.mock.ts | 1 + .../components/top_nav/get_top_nav_links.ts | 1 + .../apps/main/utils/update_search_source.ts | 5 +- ...ble_anomaly_charts_container.test.tsx.snap | 1 + .../generate_csv/generate_csv.ts | 2 +- 8 files changed, 168 insertions(+), 25 deletions(-) diff --git a/src/plugins/data/common/query/timefilter/get_time.test.ts b/src/plugins/data/common/query/timefilter/get_time.test.ts index 5389eb71a10bb..70f6f418cc739 100644 --- a/src/plugins/data/common/query/timefilter/get_time.test.ts +++ b/src/plugins/data/common/query/timefilter/get_time.test.ts @@ -7,9 +7,10 @@ */ import { RangeFilter } from '@kbn/es-query'; +import type { IIndexPattern } from '../..'; import moment from 'moment'; import sinon from 'sinon'; -import { getTime, getAbsoluteTimeRange } from './get_time'; +import { getTime, getRelativeTime, getAbsoluteTimeRange } from './get_time'; describe('get_time', () => { describe('getTime', () => { @@ -17,7 +18,7 @@ describe('get_time', () => { const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf()); const filter = getTime( - { + ({ id: 'test', title: 'test', timeFieldName: 'date', @@ -31,7 +32,7 @@ describe('get_time', () => { filterable: true, }, ], - } as any, + } as unknown) as IIndexPattern, { from: 'now-60y', to: 'now' } ) as RangeFilter; expect(filter.range.date).toEqual({ @@ -46,7 +47,7 @@ describe('get_time', () => { const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf()); const filter = getTime( - { + ({ id: 'test', title: 'test', timeFieldName: 'date', @@ -68,7 +69,7 @@ describe('get_time', () => { filterable: true, }, ], - } as any, + } as unknown) as IIndexPattern, { from: 'now-60y', to: 'now' }, { fieldName: 'myCustomDate' } ) as RangeFilter; @@ -80,6 +81,83 @@ describe('get_time', () => { clock.restore(); }); }); + describe('getRelativeTime', () => { + test('do not coerce relative time to absolute time when given flag', () => { + const filter = getRelativeTime( + ({ + id: 'test', + title: 'test', + timeFieldName: 'date', + fields: [ + { + name: 'date', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'myCustomDate', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + } as unknown) as IIndexPattern, + { from: 'now-60y', to: 'now' }, + { fieldName: 'myCustomDate' } + ) as RangeFilter; + + expect(filter.range.myCustomDate).toEqual({ + gte: 'now-60y', + lte: 'now', + format: 'strict_date_optional_time', + }); + }); + test('do not coerce relative time to absolute time when given flag - with mixed from and to times', () => { + const clock = sinon.useFakeTimers(moment.utc().valueOf()); + const filter = getRelativeTime( + ({ + id: 'test', + title: 'test', + timeFieldName: 'date', + fields: [ + { + name: 'date', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'myCustomDate', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + } as unknown) as IIndexPattern, + { + from: '2020-09-01T08:30:00.000Z', + to: 'now', + }, + { fieldName: 'myCustomDate' } + ) as RangeFilter; + + expect(filter.range.myCustomDate).toEqual({ + gte: '2020-09-01T08:30:00.000Z', + lte: 'now', + format: 'strict_date_optional_time', + }); + clock.restore(); + }); + }); describe('getAbsoluteTimeRange', () => { test('should forward absolute timerange as is', () => { const from = '2000-01-01T00:00:00.000Z'; diff --git a/src/plugins/data/common/query/timefilter/get_time.ts b/src/plugins/data/common/query/timefilter/get_time.ts index 4c20e49f53315..fd21b2251ea3a 100644 --- a/src/plugins/data/common/query/timefilter/get_time.ts +++ b/src/plugins/data/common/query/timefilter/get_time.ts @@ -7,20 +7,30 @@ */ import dateMath from '@elastic/datemath'; +import { omitBy } from 'lodash'; import { buildRangeFilter } from '@kbn/es-query'; -import type { IIndexPattern, TimeRange, TimeRangeBounds } from '../..'; +import type { Moment } from 'moment'; +import type { IIndexPattern, TimeRange, TimeRangeBounds, RangeFilterParams } from '../..'; interface CalculateBoundsOptions { forceNow?: Date; } +const calculateLowerBound = (from: string, forceNow?: Date): undefined | Moment => + dateMath.parse(from, { forceNow }); + +const calculateUpperBound = (to: string, forceNow?: Date): undefined | Moment => + dateMath.parse(to, { roundUp: true, forceNow }); + +const isRelativeTime = (value: string): boolean => value.includes('now'); + export function calculateBounds( timeRange: TimeRange, options: CalculateBoundsOptions = {} ): TimeRangeBounds { return { - min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }), - max: dateMath.parse(timeRange.to, { roundUp: true, forceNow: options.forceNow }), + min: calculateLowerBound(timeRange.from, options.forceNow), + max: calculateUpperBound(timeRange.to, options.forceNow), }; } @@ -44,7 +54,22 @@ export function getTime( indexPattern, timeRange, options?.fieldName || indexPattern?.timeFieldName, - options?.forceNow + options?.forceNow, + true + ); +} + +export function getRelativeTime( + indexPattern: IIndexPattern | undefined, + timeRange: TimeRange, + options?: { forceNow?: Date; fieldName?: string } +) { + return createTimeRangeFilter( + indexPattern, + timeRange, + options?.fieldName || indexPattern?.timeFieldName, + options?.forceNow, + false ); } @@ -52,7 +77,8 @@ function createTimeRangeFilter( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, fieldName?: string, - forceNow?: Date + forceNow?: Date, + coerceRelativeTimeToAbsoluteTime: boolean = true ) { if (!indexPattern) { return; @@ -64,17 +90,28 @@ function createTimeRangeFilter( return; } - const bounds = calculateBounds(timeRange, { forceNow }); - if (!bounds) { - return; + let rangeFilterParams: RangeFilterParams = { + format: 'strict_date_optional_time', + }; + + if (coerceRelativeTimeToAbsoluteTime) { + const bounds = calculateBounds(timeRange, { forceNow }); + if (!bounds) { + return; + } + rangeFilterParams.gte = bounds.min?.toISOString(); + rangeFilterParams.lte = bounds.max?.toISOString(); + } else { + rangeFilterParams.gte = isRelativeTime(timeRange.from) + ? timeRange.from + : calculateLowerBound(timeRange.from, forceNow)?.toISOString(); + + rangeFilterParams.lte = isRelativeTime(timeRange.to) + ? timeRange.to + : calculateUpperBound(timeRange.to, forceNow)?.toISOString(); } - return buildRangeFilter( - field, - { - ...(bounds.min && { gte: bounds.min.toISOString() }), - ...(bounds.max && { lte: bounds.max.toISOString() }), - format: 'strict_date_optional_time', - }, - indexPattern - ); + + rangeFilterParams = omitBy(rangeFilterParams, (v) => v == null); + + return buildRangeFilter(field, rangeFilterParams, indexPattern); } diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 9894010601d2b..3b537562586a7 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -17,6 +17,7 @@ import { calculateBounds, getAbsoluteTimeRange, getTime, + getRelativeTime, IIndexPattern, RefreshInterval, TimeRange, @@ -27,7 +28,6 @@ import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loo export { AutoRefreshDoneFn }; // TODO: remove! - export class Timefilter { // Fired when isTimeRangeSelectorEnabled \ isAutoRefreshSelectorEnabled are toggled private enabledUpdated$ = new BehaviorSubject(false); @@ -178,12 +178,34 @@ export class Timefilter { } }; + /** + * Create a time filter that coerces all time values to absolute time. + * + * This is useful for creating a filter that ensures all ES queries will fetch the exact same data + * and leverages ES query cache for performance improvement. + * + * One use case is keeping different elements embedded in the same UI in sync. + */ public createFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => { return getTime(indexPattern, timeRange ? timeRange : this._time, { forceNow: this.nowProvider.get(), }); }; + /** + * Create a time filter that converts only absolute time to ISO strings, it leaves relative time + * values unchanged (e.g. "now-1"). + * + * This is useful for sending datemath values to ES endpoints to generate reports over time. + * + * @note Consumers of this function need to ensure that the ES endpoint supports datemath. + */ + public createRelativeFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => { + return getRelativeTime(indexPattern, timeRange ? timeRange : this._time, { + forceNow: this.nowProvider.get(), + }); + }; + public getBounds(): TimeRangeBounds { return this.calculateBounds(this._time); } diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index f1f02a010e98c..2b6a65e6c9bd7 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -34,6 +34,7 @@ const createSetupContractMock = () => { getBounds: jest.fn(), calculateBounds: jest.fn(), createFilter: jest.fn(), + createRelativeFilter: jest.fn(), getRefreshIntervalDefaults: jest.fn(), getTimeDefaults: jest.fn(), getAbsoluteTime: jest.fn(), diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index a692dacd5e9f4..58a7242974ba2 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -114,6 +114,7 @@ export const getTopNavLinks = ({ state.appStateContainer.getState(), services.uiSettings ); + services.share.toggleShareContextMenu({ anchorElement, allowEmbed: false, diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index b4a1dab41a096..74e63c399743f 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -54,7 +54,10 @@ export function updateSearchSource( // this is not the default index pattern, it determines that it's not of type rollup if (indexPatternsUtils.isDefault(indexPattern)) { - searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern)); + searchSource.setField( + 'filter', + data.query.timefilter.timefilter.createRelativeFilter(indexPattern) + ); } if (useNewFieldsApi) { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/__snapshots__/embeddable_anomaly_charts_container.test.tsx.snap b/x-pack/plugins/ml/public/embeddables/anomaly_charts/__snapshots__/embeddable_anomaly_charts_container.test.tsx.snap index 52318c80d5d08..703a3778ebfb9 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/__snapshots__/embeddable_anomaly_charts_container.test.tsx.snap +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/__snapshots__/embeddable_anomaly_charts_container.test.tsx.snap @@ -33,6 +33,7 @@ Object { "timefilter": Object { "calculateBounds": [MockFunction], "createFilter": [MockFunction], + "createRelativeFilter": [MockFunction], "disableAutoRefreshSelector": [MockFunction], "disableTimeRangeSelector": [MockFunction], "enableAutoRefreshSelector": [MockFunction], diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 61dc9881f5bcc..dc5c560d29546 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -291,7 +291,7 @@ export class CsvGenerator { const index = searchSource.getField('index'); if (!index) { - throw new Error(`The search must have a revference to an index pattern!`); + throw new Error(`The search must have a reference to an index pattern!`); } const { maxSizeBytes, bom, escapeFormulaValues, scroll: scrollSettings } = settings; From dd3053d23e99de1ba8e58608ffecebd0d6b2638c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 14 Sep 2021 15:59:30 +0300 Subject: [PATCH 14/39] [Cases] Fix hidden lens modal when creating case from flyout (#112067) --- .../components/actions/timeline/cases/create/flyout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx index 4f189648634d0..727d853990224 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx @@ -25,7 +25,7 @@ export interface CreateCaseModalProps { const StyledFlyout = styled(EuiFlyout)` ${({ theme }) => ` - z-index: ${theme.eui.euiZModal}; + z-index: ${theme.eui.euiZLevel5}; `} `; @@ -37,10 +37,10 @@ const maskOverlayClassName = 'create-case-flyout-mask-overlay'; * A global style is needed to target a parent element. */ -const GlobalStyle = createGlobalStyle<{ theme: { eui: { euiZModal: number } } }>` +const GlobalStyle = createGlobalStyle<{ theme: { eui: { euiZLevel5: number } } }>` .${maskOverlayClassName} { ${({ theme }) => ` - z-index: ${theme.eui.euiZModal}; + z-index: ${theme.eui.euiZLevel5}; `} } `; From ec313f6373710083bfa4e32c6b9e0e198f847d40 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 14 Sep 2021 16:24:01 +0300 Subject: [PATCH 15/39] [Canvas] Fixes Advanced settings in Datasource recreate on any change. (#111840) * added useEffectOnce. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../canvas/public/components/datasource/datasource.js | 4 +++- .../canvas/public/expression_types/datasource.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource.js b/x-pack/plugins/canvas/public/components/datasource/datasource.js index acda812792c45..38d3f6d112ecc 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource.js @@ -12,7 +12,9 @@ import { DatasourceComponent } from './datasource_component'; export const Datasource = (props) => { const { datasource, stateDatasource } = props; - if (!datasource || !stateDatasource) return ; + if (!datasource || !stateDatasource) { + return ; + } return ; }; diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.tsx b/x-pack/plugins/canvas/public/expression_types/datasource.tsx index 0afb9bdd2f96a..7566c473a720a 100644 --- a/x-pack/plugins/canvas/public/expression_types/datasource.tsx +++ b/x-pack/plugins/canvas/public/expression_types/datasource.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect, useRef, useCallback } from 'react'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; import { Ast } from '@kbn/interpreter/common'; import { RenderToDom } from '../components/render_to_dom'; import { BaseForm, BaseFormProps } from './base_form'; @@ -62,10 +63,11 @@ const DatasourceWrapper: React.FunctionComponent = (prop useEffect(() => { callRenderFn(); - return () => { - handlers.destroy(); - }; - }, [callRenderFn, handlers, props]); + }, [callRenderFn, props]); + + useEffectOnce(() => () => { + handlers.destroy(); + }); return ( Date: Tue, 14 Sep 2021 15:36:37 +0200 Subject: [PATCH 16/39] remove async lifecycle from global_search_bar plugin (#112058) --- x-pack/plugins/global_search_bar/public/plugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx index 954e4241966a0..dfce09bd12edc 100644 --- a/x-pack/plugins/global_search_bar/public/plugin.tsx +++ b/x-pack/plugins/global_search_bar/public/plugin.tsx @@ -23,7 +23,7 @@ export interface GlobalSearchBarPluginStartDeps { } export class GlobalSearchBarPlugin implements Plugin<{}, {}> { - public async setup() { + public setup() { return {}; } From 4bca9b05bb99fa7884a21795fb45bfdb5c0315b6 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 14 Sep 2021 15:47:43 +0200 Subject: [PATCH 17/39] Fix wrong timepicker range when brushing (#111989) --- .../public/application/apps/main/components/chart/histogram.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx index df63c4c94ae86..bfb4cd1380766 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx @@ -159,6 +159,7 @@ export function DiscoverHistogram({ tooltip={tooltipProps} theme={chartTheme} baseTheme={chartBaseTheme} + allowBrushingLastHistogramBucket={true} /> Date: Tue, 14 Sep 2021 16:50:45 +0300 Subject: [PATCH 18/39] [core.metrics] Add support for multiple processes in ops metrics & stats API; deprecate process field (#109820) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...in-core-server.deprecationsservicesetup.md | 15 ++-- ...er.eventloopdelaysmonitor._constructor_.md | 13 +++ ...e-server.eventloopdelaysmonitor.collect.md | 19 ++++ ...ugin-core-server.eventloopdelaysmonitor.md | 26 ++++++ ...ore-server.eventloopdelaysmonitor.reset.md | 17 ++++ ...core-server.eventloopdelaysmonitor.stop.md | 17 ++++ ...n-core-server.intervalhistogram.exceeds.md | 11 +++ ...-server.intervalhistogram.fromtimestamp.md | 11 +++ ...-server.intervalhistogram.lastupdatedat.md | 11 +++ ...lugin-core-server.intervalhistogram.max.md | 11 +++ ...na-plugin-core-server.intervalhistogram.md | 27 ++++++ ...ugin-core-server.intervalhistogram.mean.md | 11 +++ ...lugin-core-server.intervalhistogram.min.md | 11 +++ ...re-server.intervalhistogram.percentiles.md | 16 ++++ ...in-core-server.intervalhistogram.stddev.md | 11 +++ .../core/server/kibana-plugin-core-server.md | 2 + .../kibana-plugin-core-server.opsmetrics.md | 3 +- ...a-plugin-core-server.opsmetrics.process.md | 6 +- ...plugin-core-server.opsmetrics.processes.md | 13 +++ ...rver.opsprocessmetrics.event_loop_delay.md | 2 +- ...ocessmetrics.event_loop_delay_histogram.md | 13 +++ ...na-plugin-core-server.opsprocessmetrics.md | 3 +- .../core_app/status/lib/load_status.test.ts | 20 ++++- .../deprecations/deprecations_service.ts | 1 - src/core/server/index.ts | 2 + .../metrics/collectors/collector.mock.ts | 6 +- src/core/server/metrics/collectors/mocks.ts | 4 +- .../metrics/collectors/process.mocks.ts | 24 +++++ .../server/metrics/collectors/process.test.ts | 40 +++++---- src/core/server/metrics/collectors/process.ts | 40 +++++---- src/core/server/metrics/collectors/types.ts | 12 ++- .../event_loop_delays/__mocks__/perf_hooks.ts | 23 +++++ .../event_loop_delays_monitor.mocks.ts} | 41 ++++----- .../event_loop_delays_monitor.test.ts | 59 ++++++++++++ .../event_loop_delays_monitor.ts} | 50 ++++++----- .../server/metrics/event_loop_delays/index.ts | 9 ++ src/core/server/metrics/index.ts | 2 + .../logging/get_ops_metrics_log.test.ts | 14 ++- .../server/metrics/metrics_service.mock.ts | 18 ++-- .../metrics/ops_metrics_collector.test.ts | 3 +- .../server/metrics/ops_metrics_collector.ts | 11 ++- src/core/server/metrics/types.ts | 43 ++++++++- src/core/server/server.api.md | 38 ++++++++ src/core/server/status/routes/status.ts | 1 + .../collectors/event_loop_delays/constants.ts | 5 -- .../event_loop_delays.test.ts | 63 ------------- .../integration_tests/daily_rollups.test.ts | 16 ++-- .../event_loop_delays/saved_objects.test.ts | 18 ++-- .../event_loop_delays/saved_objects.ts | 23 +++-- .../collectors/event_loop_delays/schema.ts | 7 ++ .../event_loop_delays/track_delays.test.ts | 81 ++++++++++------- .../event_loop_delays/track_delays.ts | 13 +-- .../event_loop_delays/track_threshold.test.ts | 69 ++++++++------ .../event_loop_delays/track_threshold.ts | 10 +-- .../__snapshots__/index.test.ts.snap | 43 --------- .../ops_stats_collector.test.ts.snap | 59 ++++++++++++ ...ex.test.ts => ops_stats_collector.test.ts} | 89 +++---------------- .../ops_stats/ops_stats_collector.ts | 5 ++ .../kibana_usage_collection/server/plugin.ts | 14 ++- src/plugins/telemetry/schema/oss_plugins.json | 6 ++ 60 files changed, 846 insertions(+), 405 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.exceeds.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.fromtimestamp.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.lastupdatedat.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.max.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.mean.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.min.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.percentiles.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.intervalhistogram.stddev.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.opsmetrics.processes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md create mode 100644 src/core/server/metrics/collectors/process.mocks.ts create mode 100644 src/core/server/metrics/event_loop_delays/__mocks__/perf_hooks.ts rename src/{plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts => core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts} (54%) create mode 100644 src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts rename src/{plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts => core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts} (58%) create mode 100644 src/core/server/metrics/event_loop_delays/index.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts delete mode 100644 src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/index.test.ts.snap create mode 100644 src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap rename src/plugins/kibana_usage_collection/server/collectors/ops_stats/{index.test.ts => ops_stats_collector.test.ts} (50%) diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md index 00e5da4a9a9f9..7b2cbdecd146a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -25,8 +25,6 @@ import { i18n } from '@kbn/i18n'; async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { const deprecations: DeprecationsDetails[] = []; - - // Example of an api correctiveAction const count = await getFooCount(savedObjectsClient); if (count > 0) { deprecations.push({ @@ -42,12 +40,12 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations level: 'warning', correctiveActions: { manualSteps: [ - i18n.translate('xpack.foo.deprecations.manualStepOneMessage', { - defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".', - }), - i18n.translate('xpack.foo.deprecations.manualStepTwoMessage', { - defaultMessage: 'Select Foo from the "New Visualization" window.', - }), + i18n.translate('xpack.foo.deprecations.manualStepOneMessage', { + defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".', + }), + i18n.translate('xpack.foo.deprecations.manualStepTwoMessage', { + defaultMessage: 'Select Foo from the "New Visualization" window.', + }), ], api: { path: '/internal/security/users/test_dashboard_user', @@ -68,7 +66,6 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations }, }); } - return deprecations; } diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor._constructor_.md new file mode 100644 index 0000000000000..ae9df8b406be6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor._constructor_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [EventLoopDelaysMonitor](./kibana-plugin-core-server.eventloopdelaysmonitor.md) > [(constructor)](./kibana-plugin-core-server.eventloopdelaysmonitor._constructor_.md) + +## EventLoopDelaysMonitor.(constructor) + +Creating a new instance from EventLoopDelaysMonitor will automatically start tracking event loop delays. + +Signature: + +```typescript +constructor(); +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md new file mode 100644 index 0000000000000..0e07497baf887 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [EventLoopDelaysMonitor](./kibana-plugin-core-server.eventloopdelaysmonitor.md) > [collect](./kibana-plugin-core-server.eventloopdelaysmonitor.collect.md) + +## EventLoopDelaysMonitor.collect() method + +Collect gathers event loop delays metrics from nodejs perf\_hooks.monitorEventLoopDelay the histogram calculations start from the last time `reset` was called or this EventLoopDelaysMonitor instance was created. + +Signature: + +```typescript +collect(): IntervalHistogram; +``` +Returns: + +`IntervalHistogram` + +{IntervalHistogram} + diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.md new file mode 100644 index 0000000000000..21bbd8b48840c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [EventLoopDelaysMonitor](./kibana-plugin-core-server.eventloopdelaysmonitor.md) + +## EventLoopDelaysMonitor class + +Signature: + +```typescript +export declare class EventLoopDelaysMonitor +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-core-server.eventloopdelaysmonitor._constructor_.md) | | Creating a new instance from EventLoopDelaysMonitor will automatically start tracking event loop delays. | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [collect()](./kibana-plugin-core-server.eventloopdelaysmonitor.collect.md) | | Collect gathers event loop delays metrics from nodejs perf\_hooks.monitorEventLoopDelay the histogram calculations start from the last time reset was called or this EventLoopDelaysMonitor instance was created. | +| [reset()](./kibana-plugin-core-server.eventloopdelaysmonitor.reset.md) | | Resets the collected histogram data. | +| [stop()](./kibana-plugin-core-server.eventloopdelaysmonitor.stop.md) | | Disables updating the interval timer for collecting new data points. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md new file mode 100644 index 0000000000000..fdba7a79ebda0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [EventLoopDelaysMonitor](./kibana-plugin-core-server.eventloopdelaysmonitor.md) > [reset](./kibana-plugin-core-server.eventloopdelaysmonitor.reset.md) + +## EventLoopDelaysMonitor.reset() method + +Resets the collected histogram data. + +Signature: + +```typescript +reset(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md new file mode 100644 index 0000000000000..25b61434b0061 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [EventLoopDelaysMonitor](./kibana-plugin-core-server.eventloopdelaysmonitor.md) > [stop](./kibana-plugin-core-server.eventloopdelaysmonitor.stop.md) + +## EventLoopDelaysMonitor.stop() method + +Disables updating the interval timer for collecting new data points. + +Signature: + +```typescript +stop(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.exceeds.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.exceeds.md new file mode 100644 index 0000000000000..664bdb8f24d7b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.exceeds.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [exceeds](./kibana-plugin-core-server.intervalhistogram.exceeds.md) + +## IntervalHistogram.exceeds property + +Signature: + +```typescript +exceeds: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.fromtimestamp.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.fromtimestamp.md new file mode 100644 index 0000000000000..00fa8dcb84430 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.fromtimestamp.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [fromTimestamp](./kibana-plugin-core-server.intervalhistogram.fromtimestamp.md) + +## IntervalHistogram.fromTimestamp property + +Signature: + +```typescript +fromTimestamp: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.lastupdatedat.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.lastupdatedat.md new file mode 100644 index 0000000000000..58e75fc2ba437 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.lastupdatedat.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [lastUpdatedAt](./kibana-plugin-core-server.intervalhistogram.lastupdatedat.md) + +## IntervalHistogram.lastUpdatedAt property + +Signature: + +```typescript +lastUpdatedAt: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.max.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.max.md new file mode 100644 index 0000000000000..14d7fe6b68c4b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.max.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [max](./kibana-plugin-core-server.intervalhistogram.max.md) + +## IntervalHistogram.max property + +Signature: + +```typescript +max: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md new file mode 100644 index 0000000000000..d7fb889dce322 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) + +## IntervalHistogram interface + +an IntervalHistogram object that samples and reports the event loop delay over time. The delays will be reported in nanoseconds. + +Signature: + +```typescript +export interface IntervalHistogram +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [exceeds](./kibana-plugin-core-server.intervalhistogram.exceeds.md) | number | | +| [fromTimestamp](./kibana-plugin-core-server.intervalhistogram.fromtimestamp.md) | string | | +| [lastUpdatedAt](./kibana-plugin-core-server.intervalhistogram.lastupdatedat.md) | string | | +| [max](./kibana-plugin-core-server.intervalhistogram.max.md) | number | | +| [mean](./kibana-plugin-core-server.intervalhistogram.mean.md) | number | | +| [min](./kibana-plugin-core-server.intervalhistogram.min.md) | number | | +| [percentiles](./kibana-plugin-core-server.intervalhistogram.percentiles.md) | {
50: number;
75: number;
95: number;
99: number;
} | | +| [stddev](./kibana-plugin-core-server.intervalhistogram.stddev.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.mean.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.mean.md new file mode 100644 index 0000000000000..e6794bfa5fe52 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.mean.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [mean](./kibana-plugin-core-server.intervalhistogram.mean.md) + +## IntervalHistogram.mean property + +Signature: + +```typescript +mean: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.min.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.min.md new file mode 100644 index 0000000000000..d0eb929601f18 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.min.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [min](./kibana-plugin-core-server.intervalhistogram.min.md) + +## IntervalHistogram.min property + +Signature: + +```typescript +min: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.percentiles.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.percentiles.md new file mode 100644 index 0000000000000..b0adc9531c0b1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.percentiles.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [percentiles](./kibana-plugin-core-server.intervalhistogram.percentiles.md) + +## IntervalHistogram.percentiles property + +Signature: + +```typescript +percentiles: { + 50: number; + 75: number; + 95: number; + 99: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.stddev.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.stddev.md new file mode 100644 index 0000000000000..bca5ab56cb237 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.stddev.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) > [stddev](./kibana-plugin-core-server.intervalhistogram.stddev.md) + +## IntervalHistogram.stddev property + +Signature: + +```typescript +stddev: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 96bb82c8968df..66c0299669dc4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -19,6 +19,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [BasePath](./kibana-plugin-core-server.basepath.md) | Access or manipulate the Kibana base path | | [CspConfig](./kibana-plugin-core-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) | Wrapper of config schema. | +| [EventLoopDelaysMonitor](./kibana-plugin-core-server.eventloopdelaysmonitor.md) | | | [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | @@ -97,6 +98,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | +| [IntervalHistogram](./kibana-plugin-core-server.intervalhistogram.md) | an IntervalHistogram object that samples and reports the event loop delay over time. The delays will be reported in nanoseconds. | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | | [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md index 9803c0fbd53cc..1572c1ae3131e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md @@ -19,7 +19,8 @@ export interface OpsMetrics | [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md) | Date | Time metrics were recorded at. | | [concurrent\_connections](./kibana-plugin-core-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics['concurrent_connections'] | number of current concurrent connections to the server | | [os](./kibana-plugin-core-server.opsmetrics.os.md) | OpsOsMetrics | OS related metrics | -| [process](./kibana-plugin-core-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics | +| [process](./kibana-plugin-core-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics. Deprecated in favor of processes field. | +| [processes](./kibana-plugin-core-server.opsmetrics.processes.md) | OpsProcessMetrics[] | Process related metrics. Reports an array of objects for each kibana pid. | | [requests](./kibana-plugin-core-server.opsmetrics.requests.md) | OpsServerMetrics['requests'] | server requests stats | | [response\_times](./kibana-plugin-core-server.opsmetrics.response_times.md) | OpsServerMetrics['response_times'] | server response time stats | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.process.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.process.md index b3759fadafc0a..9da2c0644dc84 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.process.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.process.md @@ -4,7 +4,11 @@ ## OpsMetrics.process property -Process related metrics +> Warning: This API is now obsolete. +> +> + +Process related metrics. Deprecated in favor of processes field. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.processes.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.processes.md new file mode 100644 index 0000000000000..cf1f0a7c54475 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.processes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) > [processes](./kibana-plugin-core-server.opsmetrics.processes.md) + +## OpsMetrics.processes property + +Process related metrics. Reports an array of objects for each kibana pid. + +Signature: + +```typescript +processes: OpsProcessMetrics[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md index 239f94e37d00e..d626b9cf8f98c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md @@ -4,7 +4,7 @@ ## OpsProcessMetrics.event\_loop\_delay property -node event loop delay +mean event loop delay since last collection Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md new file mode 100644 index 0000000000000..1d870b19f2d1f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsProcessMetrics](./kibana-plugin-core-server.opsprocessmetrics.md) > [event\_loop\_delay\_histogram](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md) + +## OpsProcessMetrics.event\_loop\_delay\_histogram property + +node event loop delay histogram since last collection + +Signature: + +```typescript +event_loop_delay_histogram: IntervalHistogram; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md index 79763b783470e..198b668afca60 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md @@ -16,7 +16,8 @@ export interface OpsProcessMetrics | Property | Type | Description | | --- | --- | --- | -| [event\_loop\_delay](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md) | number | node event loop delay | +| [event\_loop\_delay\_histogram](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md) | IntervalHistogram | node event loop delay histogram since last collection | +| [event\_loop\_delay](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md) | number | mean event loop delay since last collection | | [memory](./kibana-plugin-core-server.opsprocessmetrics.memory.md) | {
heap: {
total_in_bytes: number;
used_in_bytes: number;
size_limit: number;
};
resident_set_size_in_bytes: number;
} | process memory usage | | [pid](./kibana-plugin-core-server.opsprocessmetrics.pid.md) | number | pid of the kibana process | | [uptime\_in\_millis](./kibana-plugin-core-server.opsprocessmetrics.uptime_in_millis.md) | number | uptime of the kibana process | diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts index e412192ea00ee..73c697c3d55aa 100644 --- a/src/core/public/core_app/status/lib/load_status.test.ts +++ b/src/core/public/core_app/status/lib/load_status.test.ts @@ -9,6 +9,7 @@ import { StatusResponse } from '../../../../types/status'; import { httpServiceMock } from '../../../http/http_service.mock'; import { notificationServiceMock } from '../../../notifications/notifications_service.mock'; +import { mocked } from '../../../../server/metrics/event_loop_delays/event_loop_delays_monitor.mocks'; import { loadStatus } from './load_status'; const mockedResponse: StatusResponse = { @@ -61,6 +62,7 @@ const mockedResponse: StatusResponse = { }, }, process: { + pid: 1, memory: { heap: { size_limit: 1000000, @@ -70,9 +72,25 @@ const mockedResponse: StatusResponse = { resident_set_size_in_bytes: 1, }, event_loop_delay: 1, - pid: 1, + event_loop_delay_histogram: mocked.createHistogram(), uptime_in_millis: 1, }, + processes: [ + { + pid: 1, + memory: { + heap: { + size_limit: 1000000, + used_in_bytes: 100, + total_in_bytes: 0, + }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + event_loop_delay_histogram: mocked.createHistogram(), + uptime_in_millis: 1, + }, + ], response_times: { avg_in_millis: 4000, max_in_millis: 8000, diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts index b8a134fbf8cd2..bc981c21ba975 100644 --- a/src/core/server/deprecations/deprecations_service.ts +++ b/src/core/server/deprecations/deprecations_service.ts @@ -39,7 +39,6 @@ import { SavedObjectsClientContract } from '../saved_objects/types'; * const deprecations: DeprecationsDetails[] = []; * const count = await getFooCount(savedObjectsClient); * if (count > 0) { - * // Example of a manual correctiveAction * deprecations.push({ * title: i18n.translate('xpack.foo.deprecations.title', { * defaultMessage: `Foo's are deprecated` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 3a55d70109b8c..345c95c8d0773 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -378,7 +378,9 @@ export type { OpsProcessMetrics, MetricsServiceSetup, MetricsServiceStart, + IntervalHistogram, } from './metrics'; +export { EventLoopDelaysMonitor } from './metrics'; export type { I18nServiceSetup } from './i18n'; export type { diff --git a/src/core/server/metrics/collectors/collector.mock.ts b/src/core/server/metrics/collectors/collector.mock.ts index bf45925bf583f..088156fa2ff5e 100644 --- a/src/core/server/metrics/collectors/collector.mock.ts +++ b/src/core/server/metrics/collectors/collector.mock.ts @@ -8,8 +8,10 @@ import { MetricsCollector } from './types'; -const createCollector = (collectReturnValue: any = {}): jest.Mocked> => { - const collector: jest.Mocked> = { +const createCollector = ( + collectReturnValue: any = {} +): jest.Mocked> => { + const collector: jest.Mocked> = { collect: jest.fn().mockResolvedValue(collectReturnValue), reset: jest.fn(), }; diff --git a/src/core/server/metrics/collectors/mocks.ts b/src/core/server/metrics/collectors/mocks.ts index ad8dd9fa57966..425751899ddc9 100644 --- a/src/core/server/metrics/collectors/mocks.ts +++ b/src/core/server/metrics/collectors/mocks.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { MetricsCollector } from './types'; +import type { MetricsCollector } from './types'; +import { createMockOpsProcessMetrics } from './process.mocks'; const createMock = () => { const mocked: jest.Mocked> = { @@ -21,4 +22,5 @@ const createMock = () => { export const collectorMock = { create: createMock, + createOpsProcessMetrics: createMockOpsProcessMetrics, }; diff --git a/src/core/server/metrics/collectors/process.mocks.ts b/src/core/server/metrics/collectors/process.mocks.ts new file mode 100644 index 0000000000000..8ee43394b9251 --- /dev/null +++ b/src/core/server/metrics/collectors/process.mocks.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { mocked } from '../event_loop_delays/event_loop_delays_monitor.mocks'; +import type { OpsProcessMetrics } from './types'; + +export function createMockOpsProcessMetrics(): OpsProcessMetrics { + const histogram = mocked.createHistogram(); + + return { + memory: { + heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + event_loop_delay_histogram: histogram, + pid: 1, + uptime_in_millis: 1, + }; +} diff --git a/src/core/server/metrics/collectors/process.test.ts b/src/core/server/metrics/collectors/process.test.ts index 0395cd8d597fd..ff861d5c8bef1 100644 --- a/src/core/server/metrics/collectors/process.test.ts +++ b/src/core/server/metrics/collectors/process.test.ts @@ -9,6 +9,7 @@ import v8, { HeapInfo } from 'v8'; import { ProcessMetricsCollector } from './process'; +/* eslint-disable dot-notation */ describe('ProcessMetricsCollector', () => { let collector: ProcessMetricsCollector; @@ -20,28 +21,34 @@ describe('ProcessMetricsCollector', () => { jest.restoreAllMocks(); }); - it('collects pid from the process', async () => { - const metrics = await collector.collect(); + it('collects pid from the process', () => { + const metrics = collector.collect(); - expect(metrics.pid).toEqual(process.pid); + expect(metrics).toHaveLength(1); + expect(metrics[0].pid).toEqual(process.pid); }); - it('collects event loop delay', async () => { - const metrics = await collector.collect(); - - expect(metrics.event_loop_delay).toBeGreaterThan(0); + it('collects event loop delay', () => { + const mockEventLoopDelayMonitor = { collect: jest.fn().mockReturnValue({ mean: 13 }) }; + // @ts-expect-error-next-line readonly private method. + collector['eventLoopDelayMonitor'] = mockEventLoopDelayMonitor; + const metrics = collector.collect(); + expect(metrics).toHaveLength(1); + expect(metrics[0].event_loop_delay).toBe(13); + expect(mockEventLoopDelayMonitor.collect).toBeCalledTimes(1); }); - it('collects uptime info from the process', async () => { + it('collects uptime info from the process', () => { const uptime = 58986; jest.spyOn(process, 'uptime').mockImplementation(() => uptime); - const metrics = await collector.collect(); + const metrics = collector.collect(); - expect(metrics.uptime_in_millis).toEqual(uptime * 1000); + expect(metrics).toHaveLength(1); + expect(metrics[0].uptime_in_millis).toEqual(uptime * 1000); }); - it('collects memory info from the process', async () => { + it('collects memory info from the process', () => { const heapTotal = 58986; const heapUsed = 4688; const heapSizeLimit = 5788; @@ -61,11 +68,12 @@ describe('ProcessMetricsCollector', () => { } as HeapInfo) ); - const metrics = await collector.collect(); + const metrics = collector.collect(); - expect(metrics.memory.heap.total_in_bytes).toEqual(heapTotal); - expect(metrics.memory.heap.used_in_bytes).toEqual(heapUsed); - expect(metrics.memory.heap.size_limit).toEqual(heapSizeLimit); - expect(metrics.memory.resident_set_size_in_bytes).toEqual(rss); + expect(metrics).toHaveLength(1); + expect(metrics[0].memory.heap.total_in_bytes).toEqual(heapTotal); + expect(metrics[0].memory.heap.used_in_bytes).toEqual(heapUsed); + expect(metrics[0].memory.heap.size_limit).toEqual(heapSizeLimit); + expect(metrics[0].memory.resident_set_size_in_bytes).toEqual(rss); }); }); diff --git a/src/core/server/metrics/collectors/process.ts b/src/core/server/metrics/collectors/process.ts index d7ff967114f00..3acfda3e165ee 100644 --- a/src/core/server/metrics/collectors/process.ts +++ b/src/core/server/metrics/collectors/process.ts @@ -7,14 +7,26 @@ */ import v8 from 'v8'; -import { Bench } from '@hapi/hoek'; import { OpsProcessMetrics, MetricsCollector } from './types'; +import { EventLoopDelaysMonitor } from '../event_loop_delays'; -export class ProcessMetricsCollector implements MetricsCollector { - public async collect(): Promise { +export class ProcessMetricsCollector implements MetricsCollector { + static getMainThreadMetrics(processes: OpsProcessMetrics[]): undefined | OpsProcessMetrics { + /** + * Currently Kibana does not support multi-processes. + * Once we have multiple processes we can add a `name` field + * and filter on `name === 'server_worker'` to get the main thread. + */ + return processes[0]; + } + + private readonly eventLoopDelayMonitor = new EventLoopDelaysMonitor(); + + private getCurrentPidMetrics(): OpsProcessMetrics { + const eventLoopDelayHistogram = this.eventLoopDelayMonitor.collect(); const heapStats = v8.getHeapStatistics(); const memoryUsage = process.memoryUsage(); - const [eventLoopDelay] = await Promise.all([getEventLoopDelay()]); + return { memory: { heap: { @@ -25,19 +37,17 @@ export class ProcessMetricsCollector implements MetricsCollector => { - const bench = new Bench(); - return new Promise((resolve) => { - setImmediate(() => { - return resolve(bench.elapsed()); - }); - }); -}; + public reset() { + this.eventLoopDelayMonitor.reset(); + } +} diff --git a/src/core/server/metrics/collectors/types.ts b/src/core/server/metrics/collectors/types.ts index ec9746aaae769..4684e8008f4da 100644 --- a/src/core/server/metrics/collectors/types.ts +++ b/src/core/server/metrics/collectors/types.ts @@ -5,11 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { MaybePromise } from '@kbn/utility-types'; +import type { IntervalHistogram } from '../types'; /** Base interface for all metrics gatherers */ export interface MetricsCollector { /** collect the data currently gathered by the collector */ - collect(): Promise; + collect(): MaybePromise; /** reset the internal state of the collector */ reset(): void; } @@ -19,6 +21,8 @@ export interface MetricsCollector { * @public */ export interface OpsProcessMetrics { + /** pid of the kibana process */ + pid: number; /** process memory usage */ memory: { /** heap memory usage */ @@ -33,10 +37,10 @@ export interface OpsProcessMetrics { /** node rss */ resident_set_size_in_bytes: number; }; - /** node event loop delay */ + /** mean event loop delay since last collection*/ event_loop_delay: number; - /** pid of the kibana process */ - pid: number; + /** node event loop delay histogram since last collection */ + event_loop_delay_histogram: IntervalHistogram; /** uptime of the kibana process */ uptime_in_millis: number; } diff --git a/src/core/server/metrics/event_loop_delays/__mocks__/perf_hooks.ts b/src/core/server/metrics/event_loop_delays/__mocks__/perf_hooks.ts new file mode 100644 index 0000000000000..2a5477a1c4e9e --- /dev/null +++ b/src/core/server/metrics/event_loop_delays/__mocks__/perf_hooks.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mocked } from '../event_loop_delays_monitor.mocks'; + +export const monitorEventLoopDelay = jest.fn().mockImplementation(() => { + const mockedHistogram = mocked.createHistogram(); + + return { + ...mockedHistogram, + enable: jest.fn(), + percentile: jest.fn().mockImplementation((percentile: number) => { + return (mockedHistogram.percentiles as Record)[`${percentile}`]; + }), + disable: jest.fn(), + reset: jest.fn(), + }; +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts similarity index 54% rename from src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts rename to src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts index f266a27a7034f..ee96668cf3e7c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts @@ -6,26 +6,11 @@ * Side Public License, v 1. */ import moment from 'moment'; -import type { IntervalHistogram } from './event_loop_delays'; - -export const mockMonitorEnable = jest.fn(); -export const mockMonitorPercentile = jest.fn(); -export const mockMonitorReset = jest.fn(); -export const mockMonitorDisable = jest.fn(); -export const monitorEventLoopDelay = jest.fn().mockReturnValue({ - enable: mockMonitorEnable, - percentile: mockMonitorPercentile, - disable: mockMonitorDisable, - reset: mockMonitorReset, - ...createMockHistogram(), -}); - -jest.doMock('perf_hooks', () => ({ - monitorEventLoopDelay, -})); +import type { EventLoopDelaysMonitor } from './event_loop_delays_monitor'; +import type { IntervalHistogram } from '../types'; function createMockHistogram(overwrites: Partial = {}): IntervalHistogram { - const now = moment(); + const now = Date.now(); return { min: 9093120, @@ -33,8 +18,8 @@ function createMockHistogram(overwrites: Partial = {}): Inter mean: 11993238.600747818, exceeds: 0, stddev: 1168191.9357543814, - fromTimestamp: now.startOf('day').toISOString(), - lastUpdatedAt: now.toISOString(), + fromTimestamp: moment(now).toISOString(), + lastUpdatedAt: moment(now).toISOString(), percentiles: { '50': 12607487, '75': 12615679, @@ -45,6 +30,22 @@ function createMockHistogram(overwrites: Partial = {}): Inter }; } +function createMockEventLoopDelaysMonitor() { + const mockCollect = jest.fn(); + const MockEventLoopDelaysMonitor: jest.MockedClass< + typeof EventLoopDelaysMonitor + > = jest.fn().mockReturnValue({ + collect: mockCollect, + reset: jest.fn(), + stop: jest.fn(), + }); + + mockCollect.mockReturnValue(createMockHistogram()); + + return new MockEventLoopDelaysMonitor(); +} + export const mocked = { createHistogram: createMockHistogram, + createEventLoopDelaysMonitor: createMockEventLoopDelaysMonitor, }; diff --git a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts new file mode 100644 index 0000000000000..3e88dbca8f4e7 --- /dev/null +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable dot-notation */ +jest.mock('perf_hooks'); +import { monitorEventLoopDelay } from 'perf_hooks'; +import { EventLoopDelaysMonitor } from './event_loop_delays_monitor'; +import { mocked } from './event_loop_delays_monitor.mocks'; + +describe('EventLoopDelaysMonitor', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + }); + afterEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + test('#constructor enables monitoring', () => { + const eventLoopDelaysMonitor = new EventLoopDelaysMonitor(); + expect(monitorEventLoopDelay).toBeCalledTimes(1); + expect(eventLoopDelaysMonitor['loopMonitor'].enable).toBeCalledTimes(1); + }); + + test('#collect returns event loop delays histogram', () => { + const eventLoopDelaysMonitor = new EventLoopDelaysMonitor(); + expect(eventLoopDelaysMonitor['loopMonitor'].disable).toBeCalledTimes(0); + expect(eventLoopDelaysMonitor['loopMonitor'].enable).toBeCalledTimes(1); + const histogramData = eventLoopDelaysMonitor.collect(); + expect(eventLoopDelaysMonitor['loopMonitor'].disable).toBeCalledTimes(1); + expect(eventLoopDelaysMonitor['loopMonitor'].enable).toBeCalledTimes(2); + expect(eventLoopDelaysMonitor['loopMonitor'].percentile).toHaveBeenNthCalledWith(1, 50); + expect(eventLoopDelaysMonitor['loopMonitor'].percentile).toHaveBeenNthCalledWith(2, 75); + expect(eventLoopDelaysMonitor['loopMonitor'].percentile).toHaveBeenNthCalledWith(3, 95); + expect(eventLoopDelaysMonitor['loopMonitor'].percentile).toHaveBeenNthCalledWith(4, 99); + + // mocked perf_hook returns `mocked.createHistogram()`. + // This ensures that the wiring of the `collect` function is correct. + const mockedHistogram = mocked.createHistogram(); + expect(histogramData).toEqual(mockedHistogram); + }); + + test('#reset resets histogram data', () => { + const eventLoopDelaysMonitor = new EventLoopDelaysMonitor(); + eventLoopDelaysMonitor.reset(); + expect(eventLoopDelaysMonitor['loopMonitor'].reset).toBeCalledTimes(1); + }); + test('#stop disables monitoring event loop delays', () => { + const eventLoopDelaysMonitor = new EventLoopDelaysMonitor(); + expect(eventLoopDelaysMonitor['loopMonitor'].disable).toBeCalledTimes(0); + eventLoopDelaysMonitor.stop(); + expect(eventLoopDelaysMonitor['loopMonitor'].disable).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts similarity index 58% rename from src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts rename to src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts index f5de44a061d5a..3dff847f83c9b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts @@ -8,48 +8,41 @@ import type { EventLoopDelayMonitor } from 'perf_hooks'; import { monitorEventLoopDelay } from 'perf_hooks'; -import { MONITOR_EVENT_LOOP_DELAYS_RESOLUTION } from './constants'; +import type { IntervalHistogram } from '../types'; -export interface IntervalHistogram { - fromTimestamp: string; - lastUpdatedAt: string; - min: number; - max: number; - mean: number; - exceeds: number; - stddev: number; - percentiles: { - 50: number; - 75: number; - 95: number; - 99: number; - }; -} - -export class EventLoopDelaysCollector { +export class EventLoopDelaysMonitor { private readonly loopMonitor: EventLoopDelayMonitor; private fromTimestamp: Date; + /** + * Creating a new instance from EventLoopDelaysMonitor will + * automatically start tracking event loop delays. + */ constructor() { - const monitor = monitorEventLoopDelay({ - resolution: MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, - }); + const monitor = monitorEventLoopDelay(); monitor.enable(); this.fromTimestamp = new Date(); this.loopMonitor = monitor; } - + /** + * Collect gathers event loop delays metrics from nodejs perf_hooks.monitorEventLoopDelay + * the histogram calculations start from the last time `reset` was called or this + * EventLoopDelaysMonitor instance was created. + * @returns {IntervalHistogram} + */ public collect(): IntervalHistogram { + const lastUpdated = new Date(); + this.loopMonitor.disable(); const { min, max, mean, exceeds, stddev } = this.loopMonitor; - return { + const collectedData: IntervalHistogram = { min, max, mean, exceeds, stddev, fromTimestamp: this.fromTimestamp.toISOString(), - lastUpdatedAt: new Date().toISOString(), + lastUpdatedAt: lastUpdated.toISOString(), percentiles: { 50: this.loopMonitor.percentile(50), 75: this.loopMonitor.percentile(75), @@ -57,13 +50,22 @@ export class EventLoopDelaysCollector { 99: this.loopMonitor.percentile(99), }, }; + + this.loopMonitor.enable(); + return collectedData; } + /** + * Resets the collected histogram data. + */ public reset() { this.loopMonitor.reset(); this.fromTimestamp = new Date(); } + /** + * Disables updating the interval timer for collecting new data points. + */ public stop() { this.loopMonitor.disable(); } diff --git a/src/core/server/metrics/event_loop_delays/index.ts b/src/core/server/metrics/event_loop_delays/index.ts new file mode 100644 index 0000000000000..bc9cda18d443b --- /dev/null +++ b/src/core/server/metrics/event_loop_delays/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { EventLoopDelaysMonitor } from './event_loop_delays_monitor'; diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts index 0631bb2b35801..797a0ae8c3f00 100644 --- a/src/core/server/metrics/index.ts +++ b/src/core/server/metrics/index.ts @@ -12,8 +12,10 @@ export type { MetricsServiceSetup, MetricsServiceStart, OpsMetrics, + IntervalHistogram, } from './types'; export type { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; export { MetricsService } from './metrics_service'; export { opsConfig } from './ops_config'; export type { OpsConfigType } from './ops_config'; +export { EventLoopDelaysMonitor } from './event_loop_delays'; diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts index e535b9babf92b..2d7a6bebf255e 100644 --- a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts +++ b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts @@ -8,19 +8,15 @@ import { OpsMetrics } from '..'; import { getEcsOpsMetricsLog } from './get_ops_metrics_log'; +import { collectorMock } from '../collectors/mocks'; function createBaseOpsMetrics(): OpsMetrics { + const mockProcess = collectorMock.createOpsProcessMetrics(); + return { collected_at: new Date('2020-01-01 01:00:00'), - process: { - memory: { - heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, - resident_set_size_in_bytes: 1, - }, - event_loop_delay: 1, - pid: 1, - uptime_in_millis: 1, - }, + process: mockProcess, + processes: [mockProcess], os: { platform: 'darwin' as const, platformRelease: 'test', diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index b023824df64d3..5c41e78d8efcb 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -8,8 +8,9 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; - import type { MetricsService } from './metrics_service'; +import { collectorMock } from './collectors/mocks'; +import { mocked as eventLoopDelaysMonitorMock } from './event_loop_delays/event_loop_delays_monitor.mocks'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, @@ -22,18 +23,14 @@ const createInternalSetupContractMock = () => { collectionInterval: 30000, getOpsMetrics$: jest.fn(), }; + + const processMock = collectorMock.createOpsProcessMetrics(); + setupContract.getOpsMetrics$.mockReturnValue( new BehaviorSubject({ collected_at: new Date('2020-01-01 01:00:00'), - process: { - memory: { - heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, - resident_set_size_in_bytes: 1, - }, - event_loop_delay: 1, - pid: 1, - uptime_in_millis: 1, - }, + process: processMock, + processes: [processMock], os: { platform: 'darwin' as const, platformRelease: 'test', @@ -81,4 +78,5 @@ export const metricsServiceMock = { createStartContract: createStartContractMock, createInternalSetupContract: createInternalSetupContractMock, createInternalStartContract: createInternalStartContractMock, + createEventLoopDelaysMonitor: eventLoopDelaysMonitorMock.createEventLoopDelaysMonitor, }; diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts index 3faa771db1dae..7d263d8b7d6af 100644 --- a/src/core/server/metrics/ops_metrics_collector.test.ts +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -28,7 +28,7 @@ describe('OpsMetricsCollector', () => { describe('#collect', () => { it('gathers metrics from the underlying collectors', async () => { mockOsCollector.collect.mockResolvedValue('osMetrics'); - mockProcessCollector.collect.mockResolvedValue('processMetrics'); + mockProcessCollector.collect.mockResolvedValue(['processMetrics']); mockServerCollector.collect.mockResolvedValue({ requests: 'serverRequestsMetrics', response_times: 'serverTimingMetrics', @@ -43,6 +43,7 @@ describe('OpsMetricsCollector', () => { expect(metrics).toEqual({ collected_at: expect.any(Date), process: 'processMetrics', + processes: ['processMetrics'], os: 'osMetrics', requests: 'serverRequestsMetrics', response_times: 'serverTimingMetrics', diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts index 74e8b8246d83c..376b10dcccdda 100644 --- a/src/core/server/metrics/ops_metrics_collector.ts +++ b/src/core/server/metrics/ops_metrics_collector.ts @@ -28,14 +28,21 @@ export class OpsMetricsCollector implements MetricsCollector { } public async collect(): Promise { - const [process, os, server] = await Promise.all([ + const [processes, os, server] = await Promise.all([ this.processCollector.collect(), this.osCollector.collect(), this.serverCollector.collect(), ]); + return { collected_at: new Date(), - process, + /** + * Kibana does not yet support multi-process nodes. + * `processes` is just an Array(1) only returning the current process's data + * which is why we can just use processes[0] for `process` + */ + process: processes[0], + processes, os, ...server, }; diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts index d70b8c9ff4208..550a60d0d295a 100644 --- a/src/core/server/metrics/types.ts +++ b/src/core/server/metrics/types.ts @@ -7,7 +7,7 @@ */ import { Observable } from 'rxjs'; -import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; +import type { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; /** * APIs to retrieves metrics gathered and exposed by the core platform. @@ -51,8 +51,13 @@ export type InternalMetricsServiceStart = MetricsServiceStart; export interface OpsMetrics { /** Time metrics were recorded at. */ collected_at: Date; - /** Process related metrics */ + /** + * Process related metrics. + * @deprecated use the processes field instead. + */ process: OpsProcessMetrics; + /** Process related metrics. Reports an array of objects for each kibana pid.*/ + processes: OpsProcessMetrics[]; /** OS related metrics */ os: OpsOsMetrics; /** server response time stats */ @@ -62,3 +67,37 @@ export interface OpsMetrics { /** number of current concurrent connections to the server */ concurrent_connections: OpsServerMetrics['concurrent_connections']; } + +/** + * an IntervalHistogram object that samples and reports the event loop delay over time. + * The delays will be reported in nanoseconds. + * + * @public + */ +export interface IntervalHistogram { + // The first timestamp the interval timer kicked in for collecting data points. + fromTimestamp: string; + // Last timestamp the interval timer kicked in for collecting data points. + lastUpdatedAt: string; + // The minimum recorded event loop delay. + min: number; + // The maximum recorded event loop delay. + max: number; + // The mean of the recorded event loop delays. + mean: number; + // The number of times the event loop delay exceeded the maximum 1 hour event loop delay threshold. + exceeds: number; + // The standard deviation of the recorded event loop delays. + stddev: number; + // An object detailing the accumulated percentile distribution. + percentiles: { + // 50th percentile of delays of the collected data points. + 50: number; + // 75th percentile of delays of the collected data points. + 75: number; + // 95th percentile of delays of the collected data points. + 95: number; + // 99th percentile of delays of the collected data points. + 99: number; + }; +} diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e48ec859e80a2..ea2b9dde949b2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -938,6 +938,16 @@ export interface ErrorHttpResponseOptions { headers?: ResponseHeaders; } +// Warning: (ae-missing-release-tag) "EventLoopDelaysMonitor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class EventLoopDelaysMonitor { + constructor(); + collect(): IntervalHistogram; + reset(): void; + stop(): void; +} + // @public (undocumented) export interface ExecutionContextSetup { withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; @@ -1171,6 +1181,31 @@ export interface IKibanaSocket { }): Promise; } +// @public +export interface IntervalHistogram { + // (undocumented) + exceeds: number; + // (undocumented) + fromTimestamp: string; + // (undocumented) + lastUpdatedAt: string; + // (undocumented) + max: number; + // (undocumented) + mean: number; + // (undocumented) + min: number; + // (undocumented) + percentiles: { + 50: number; + 75: number; + 95: number; + 99: number; + }; + // (undocumented) + stddev: number; +} + // @public (undocumented) export interface IRenderOptions { includeUserSettings?: boolean; @@ -1456,7 +1491,9 @@ export interface OpsMetrics { collected_at: Date; concurrent_connections: OpsServerMetrics['concurrent_connections']; os: OpsOsMetrics; + // @deprecated process: OpsProcessMetrics; + processes: OpsProcessMetrics[]; requests: OpsServerMetrics['requests']; response_times: OpsServerMetrics['response_times']; } @@ -1497,6 +1534,7 @@ export interface OpsOsMetrics { // @public export interface OpsProcessMetrics { event_loop_delay: number; + event_loop_delay_histogram: IntervalHistogram; memory: { heap: { total_in_bytes: number; diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts index 861b41c58a893..cef5ee05ea2e5 100644 --- a/src/core/server/status/routes/status.ts +++ b/src/core/server/status/routes/status.ts @@ -127,6 +127,7 @@ export const registerStatusRoute = ({ collection_interval_in_millis: metrics.collectionInterval, os: lastMetrics.os, process: lastMetrics.process, + processes: lastMetrics.processes, response_times: lastMetrics.response_times, concurrent_connections: lastMetrics.concurrent_connections, requests: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts index d6201deff5fec..4661441a15a6b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts @@ -31,11 +31,6 @@ export const MONITOR_EVENT_LOOP_DELAYS_RESET = 24 * 60 * 60 * 1000; */ export const MONITOR_EVENT_LOOP_DELAYS_START = 1 * 60 * 1000; -/** - * Event loop monitoring sampling rate in milliseconds. - */ -export const MONITOR_EVENT_LOOP_DELAYS_RESOLUTION = 10; - /** * Mean event loop delay threshold for logging a warning. */ diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts deleted file mode 100644 index b40030e210176..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts +++ /dev/null @@ -1,63 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - mockMonitorEnable, - mockMonitorPercentile, - monitorEventLoopDelay, - mockMonitorReset, - mockMonitorDisable, -} from './event_loop_delays.mocks'; -import { EventLoopDelaysCollector } from './event_loop_delays'; - -describe('EventLoopDelaysCollector', () => { - jest.useFakeTimers('modern'); - const mockNow = jest.getRealSystemTime(); - jest.setSystemTime(mockNow); - - beforeEach(() => jest.clearAllMocks()); - afterAll(() => jest.useRealTimers()); - - test('#constructor enables monitoring', () => { - new EventLoopDelaysCollector(); - expect(monitorEventLoopDelay).toBeCalledWith({ resolution: 10 }); - expect(mockMonitorEnable).toBeCalledTimes(1); - }); - - test('#collect returns event loop delays histogram', () => { - const eventLoopDelaysCollector = new EventLoopDelaysCollector(); - const histogramData = eventLoopDelaysCollector.collect(); - expect(mockMonitorPercentile).toHaveBeenNthCalledWith(1, 50); - expect(mockMonitorPercentile).toHaveBeenNthCalledWith(2, 75); - expect(mockMonitorPercentile).toHaveBeenNthCalledWith(3, 95); - expect(mockMonitorPercentile).toHaveBeenNthCalledWith(4, 99); - - expect(Object.keys(histogramData)).toMatchInlineSnapshot(` - Array [ - "min", - "max", - "mean", - "exceeds", - "stddev", - "fromTimestamp", - "lastUpdatedAt", - "percentiles", - ] - `); - }); - test('#reset resets histogram data', () => { - const eventLoopDelaysCollector = new EventLoopDelaysCollector(); - eventLoopDelaysCollector.reset(); - expect(mockMonitorReset).toBeCalledTimes(1); - }); - test('#stop disables monitoring event loop delays', () => { - const eventLoopDelaysCollector = new EventLoopDelaysCollector(); - eventLoopDelaysCollector.stop(); - expect(mockMonitorDisable).toBeCalledTimes(1); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts index 94840ccfc2748..64668a5f23de1 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -14,7 +14,7 @@ import { createRootWithCorePlugins, } from '../../../../../../../core/test_helpers/kbn_server'; import { rollDailyData } from '../daily'; -import { mocked } from '../../event_loop_delays.mocks'; +import { metricsServiceMock } from '../../../../../../../core/server/mocks'; import { SAVED_OBJECTS_DAILY_TYPE, @@ -26,18 +26,20 @@ import moment from 'moment'; const { startES } = createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), }); - +const eventLoopDelaysMonitor = metricsServiceMock.createEventLoopDelaysMonitor(); function createRawObject(date: moment.MomentInput) { const pid = Math.round(Math.random() * 10000); + const instanceUuid = 'mock_instance'; + return { type: SAVED_OBJECTS_DAILY_TYPE, - id: serializeSavedObjectId({ pid, date }), + id: serializeSavedObjectId({ pid, date, instanceUuid }), attributes: { - ...mocked.createHistogram({ - fromTimestamp: moment(date).startOf('day').toISOString(), - lastUpdatedAt: moment(date).toISOString(), - }), + ...eventLoopDelaysMonitor.collect(), + fromTimestamp: moment(date).startOf('day').toISOString(), + lastUpdatedAt: moment(date).toISOString(), processId: pid, + instanceUuid, }, }; } diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts index 022040615bd45..ddae0ff302829 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts @@ -11,21 +11,19 @@ import { serializeSavedObjectId, deleteHistogramSavedObjects, } from './saved_objects'; -import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import { savedObjectsRepositoryMock, metricsServiceMock } from '../../../../../core/server/mocks'; import type { SavedObjectsFindResponse } from '../../../../../core/server/'; -import { mocked } from './event_loop_delays.mocks'; describe('serializeSavedObjectId', () => { it('returns serialized id', () => { - const id = serializeSavedObjectId({ date: 1623233091278, pid: 123 }); - expect(id).toBe('123::09062021'); + const id = serializeSavedObjectId({ instanceUuid: 'mock_uuid', date: 1623233091278, pid: 123 }); + expect(id).toBe('mock_uuid::123::09062021'); }); }); describe('storeHistogram', () => { - const mockHistogram = mocked.createHistogram(); + const eventLoopDelaysMonitor = metricsServiceMock.createEventLoopDelaysMonitor(); const mockInternalRepository = savedObjectsRepositoryMock.create(); - jest.useFakeTimers('modern'); const mockNow = jest.getRealSystemTime(); jest.setSystemTime(mockNow); @@ -34,13 +32,15 @@ describe('storeHistogram', () => { afterAll(() => jest.useRealTimers()); it('stores histogram data in a savedObject', async () => { - await storeHistogram(mockHistogram, mockInternalRepository); + const mockHistogram = eventLoopDelaysMonitor.collect(); + const instanceUuid = 'mock_uuid'; + await storeHistogram(mockHistogram, mockInternalRepository, instanceUuid); const pid = process.pid; - const id = serializeSavedObjectId({ date: mockNow, pid }); + const id = serializeSavedObjectId({ date: mockNow, pid, instanceUuid }); expect(mockInternalRepository.create).toBeCalledWith( 'event_loop_delays_daily', - { ...mockHistogram, processId: pid }, + { ...mockHistogram, processId: pid, instanceUuid }, { id, overwrite: true } ); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts index 610a6697da364..57a9bb3b739c0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts @@ -12,12 +12,12 @@ import type { ISavedObjectsRepository, } from 'kibana/server'; import moment from 'moment'; -import type { IntervalHistogram } from './event_loop_delays'; - +import type { IntervalHistogram } from 'kibana/server'; export const SAVED_OBJECTS_DAILY_TYPE = 'event_loop_delays_daily'; export interface EventLoopDelaysDaily extends SavedObjectAttributes, IntervalHistogram { processId: number; + instanceUuid: string; } export function registerSavedObjectTypes(registerType: SavedObjectsServiceSetup['registerType']) { @@ -35,10 +35,18 @@ export function registerSavedObjectTypes(registerType: SavedObjectsServiceSetup[ }); } -export function serializeSavedObjectId({ date, pid }: { date: moment.MomentInput; pid: number }) { +export function serializeSavedObjectId({ + date, + pid, + instanceUuid, +}: { + date: moment.MomentInput; + pid: number; + instanceUuid: string; +}) { const formattedDate = moment(date).format('DDMMYYYY'); - return `${pid}::${formattedDate}`; + return `${instanceUuid}::${pid}::${formattedDate}`; } export async function deleteHistogramSavedObjects( @@ -59,14 +67,15 @@ export async function deleteHistogramSavedObjects( export async function storeHistogram( histogram: IntervalHistogram, - internalRepository: ISavedObjectsRepository + internalRepository: ISavedObjectsRepository, + instanceUuid: string ) { const pid = process.pid; - const id = serializeSavedObjectId({ date: histogram.lastUpdatedAt, pid }); + const id = serializeSavedObjectId({ date: histogram.lastUpdatedAt, pid, instanceUuid }); return await internalRepository.create( SAVED_OBJECTS_DAILY_TYPE, - { ...histogram, processId: pid }, + { ...histogram, processId: pid, instanceUuid }, { id, overwrite: true } ); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts index 319e8c77438b8..757e96e5602f0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts @@ -11,6 +11,7 @@ import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; export interface EventLoopDelaysUsageReport { daily: Array<{ processId: number; + instanceUuid: string; lastUpdatedAt: string; fromTimestamp: string; min: number; @@ -37,6 +38,12 @@ export const eventLoopDelaysUsageSchema: MakeSchemaFrom { + const eventLoopDelaysMonitor = metricsServiceMock.createEventLoopDelaysMonitor(); const mockInternalRepository = savedObjectsRepositoryMock.create(); const stopMonitoringEventLoop$ = new Subject(); + const instanceUuid = 'mock_uuid'; beforeAll(() => jest.useFakeTimers('modern')); beforeEach(() => jest.clearAllMocks()); afterEach(() => stopMonitoringEventLoop$.next()); - it('initializes EventLoopDelaysCollector and starts timer', () => { + it('collects eventLoopDelaysMonitor metrics after start delay', () => { const collectionStartDelay = 1000; - startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$, { - collectionStartDelay, - }); + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + instanceUuid, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor, + { + collectionStartDelay, + } + ); - expect(monitorEventLoopDelay).toBeCalledTimes(1); - expect(mockMonitorPercentile).toBeCalledTimes(0); + expect(eventLoopDelaysMonitor.collect).toBeCalledTimes(0); jest.advanceTimersByTime(collectionStartDelay); - expect(mockMonitorPercentile).toBeCalled(); + expect(eventLoopDelaysMonitor.collect).toBeCalledTimes(1); }); it('stores event loop delays every collectionInterval duration', () => { const collectionStartDelay = 100; const collectionInterval = 1000; - startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$, { - collectionStartDelay, - collectionInterval, - }); + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + instanceUuid, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor, + { + collectionStartDelay, + collectionInterval, + } + ); expect(mockInternalRepository.create).toBeCalledTimes(0); jest.advanceTimersByTime(collectionStartDelay); @@ -54,28 +60,39 @@ describe('startTrackingEventLoopDelaysUsage', () => { expect(mockInternalRepository.create).toBeCalledTimes(3); }); - it('resets histogram every histogramReset duration', () => { + it('resets eventLoopDelaysMonitor every histogramReset duration', () => { const collectionStartDelay = 0; const collectionInterval = 1000; const histogramReset = 5000; - startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$, { - collectionStartDelay, - collectionInterval, - histogramReset, - }); - expect(mockMonitorReset).toBeCalledTimes(0); + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + instanceUuid, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor, + { + collectionStartDelay, + collectionInterval, + histogramReset, + } + ); + + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(0); jest.advanceTimersByTime(collectionInterval * 5); - expect(mockMonitorReset).toBeCalledTimes(1); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(1); jest.advanceTimersByTime(collectionInterval * 5); - expect(mockMonitorReset).toBeCalledTimes(2); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(2); }); it('stops monitoring event loop delays once stopMonitoringEventLoop$.next is called', () => { - startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$); - - expect(mockMonitorDisable).toBeCalledTimes(0); + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + instanceUuid, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor + ); + expect(eventLoopDelaysMonitor.stop).toBeCalledTimes(0); stopMonitoringEventLoop$.next(); - expect(mockMonitorDisable).toBeCalledTimes(1); + expect(eventLoopDelaysMonitor.stop).toBeCalledTimes(1); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_delays.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_delays.ts index 70638d3b07cbc..facdb549d0df7 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_delays.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_delays.ts @@ -9,13 +9,13 @@ import { takeUntil, finalize, map } from 'rxjs/operators'; import { Observable, timer } from 'rxjs'; import type { ISavedObjectsRepository } from 'kibana/server'; +import type { EventLoopDelaysMonitor } from '../../../../../core/server'; import { MONITOR_EVENT_LOOP_DELAYS_START, MONITOR_EVENT_LOOP_DELAYS_INTERVAL, MONITOR_EVENT_LOOP_DELAYS_RESET, } from './constants'; import { storeHistogram } from './saved_objects'; -import { EventLoopDelaysCollector } from './event_loop_delays'; /** * The monitoring of the event loop starts immediately. @@ -24,7 +24,9 @@ import { EventLoopDelaysCollector } from './event_loop_delays'; */ export function startTrackingEventLoopDelaysUsage( internalRepository: ISavedObjectsRepository, + instanceUuid: string, stopMonitoringEventLoop$: Observable, + eventLoopDelaysMonitor: EventLoopDelaysMonitor, configs: { collectionStartDelay?: number; collectionInterval?: number; @@ -37,20 +39,19 @@ export function startTrackingEventLoopDelaysUsage( histogramReset = MONITOR_EVENT_LOOP_DELAYS_RESET, } = configs; - const eventLoopDelaysCollector = new EventLoopDelaysCollector(); const resetOnCount = Math.ceil(histogramReset / collectionInterval); timer(collectionStartDelay, collectionInterval) .pipe( map((i) => (i + 1) % resetOnCount === 0), takeUntil(stopMonitoringEventLoop$), - finalize(() => eventLoopDelaysCollector.stop()) + finalize(() => eventLoopDelaysMonitor.stop()) ) .subscribe(async (shouldReset) => { - const histogram = eventLoopDelaysCollector.collect(); + const histogram = eventLoopDelaysMonitor.collect(); if (shouldReset) { - eventLoopDelaysCollector.reset(); + eventLoopDelaysMonitor.reset(); } - await storeHistogram(histogram, internalRepository); + await storeHistogram(histogram, internalRepository, instanceUuid); }); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.test.ts index 1ff49a735a775..49b1943033719 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.test.ts @@ -7,13 +7,8 @@ */ import { Subject } from 'rxjs'; -import { - mockMonitorPercentile, - monitorEventLoopDelay, - mockMonitorReset, -} from './event_loop_delays.mocks'; +import { loggingSystemMock, metricsServiceMock } from '../../../../../core/server/mocks'; import { startTrackingEventLoopDelaysThreshold } from './track_threshold'; -import { loggingSystemMock } from '../../../../../core/server/mocks'; import { usageCountersServiceMock } from '../../../../usage_collection/server/usage_counters/usage_counters_service.mock'; describe('startTrackingEventLoopDelaysThreshold', () => { @@ -21,6 +16,7 @@ describe('startTrackingEventLoopDelaysThreshold', () => { const stopMonitoringEventLoop$ = new Subject(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockEventLoopCounter = mockUsageCountersSetup.createUsageCounter('testCounter'); + const eventLoopDelaysMonitor = metricsServiceMock.createEventLoopDelaysMonitor(); beforeAll(() => jest.useFakeTimers('modern')); beforeEach(() => jest.clearAllMocks()); @@ -29,15 +25,20 @@ describe('startTrackingEventLoopDelaysThreshold', () => { it('initializes EventLoopDelaysCollector and starts timer', () => { const collectionStartDelay = 1000; const warnThreshold = 1000; - startTrackingEventLoopDelaysThreshold(mockEventLoopCounter, logger, stopMonitoringEventLoop$, { - warnThreshold, - collectionStartDelay, - }); + startTrackingEventLoopDelaysThreshold( + mockEventLoopCounter, + logger, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor, + { + warnThreshold, + collectionStartDelay, + } + ); - expect(monitorEventLoopDelay).toBeCalledTimes(1); - expect(mockMonitorPercentile).toBeCalledTimes(0); + expect(eventLoopDelaysMonitor.collect).toBeCalledTimes(0); jest.advanceTimersByTime(collectionStartDelay); - expect(mockMonitorPercentile).toBeCalled(); + expect(eventLoopDelaysMonitor.collect).toBeCalledTimes(1); }); it('logs a warning and increments usage counter when the mean delay exceeds the threshold', () => { @@ -45,48 +46,60 @@ describe('startTrackingEventLoopDelaysThreshold', () => { const collectionInterval = 1000; const warnThreshold = 10; - startTrackingEventLoopDelaysThreshold(mockEventLoopCounter, logger, stopMonitoringEventLoop$, { - warnThreshold, - collectionStartDelay, - collectionInterval, - }); + startTrackingEventLoopDelaysThreshold( + mockEventLoopCounter, + logger, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor, + { + warnThreshold, + collectionStartDelay, + collectionInterval, + } + ); expect(logger.warn).toBeCalledTimes(0); expect(mockEventLoopCounter.incrementCounter).toBeCalledTimes(0); - expect(mockMonitorReset).toBeCalledTimes(0); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(0); jest.advanceTimersByTime(collectionStartDelay); expect(logger.warn).toBeCalledTimes(1); expect(mockEventLoopCounter.incrementCounter).toBeCalledTimes(1); - expect(mockMonitorReset).toBeCalledTimes(1); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(1); jest.advanceTimersByTime(collectionInterval); expect(logger.warn).toBeCalledTimes(2); expect(mockEventLoopCounter.incrementCounter).toBeCalledTimes(2); - expect(mockMonitorReset).toBeCalledTimes(2); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(2); jest.advanceTimersByTime(collectionInterval); expect(mockEventLoopCounter.incrementCounter).toBeCalledTimes(3); expect(logger.warn).toBeCalledTimes(3); - expect(mockMonitorReset).toBeCalledTimes(3); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(3); }); it('does not log warning or increment usage if threshold did not exceed mean delay', () => { const collectionStartDelay = 100; const warnThreshold = 15; - startTrackingEventLoopDelaysThreshold(mockEventLoopCounter, logger, stopMonitoringEventLoop$, { - warnThreshold, - collectionStartDelay, - }); + startTrackingEventLoopDelaysThreshold( + mockEventLoopCounter, + logger, + stopMonitoringEventLoop$, + eventLoopDelaysMonitor, + { + warnThreshold, + collectionStartDelay, + } + ); expect(logger.warn).toBeCalledTimes(0); expect(mockEventLoopCounter.incrementCounter).toBeCalledTimes(0); - expect(mockMonitorReset).toBeCalledTimes(0); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(0); jest.advanceTimersByTime(collectionStartDelay); expect(logger.warn).toBeCalledTimes(0); expect(mockEventLoopCounter.incrementCounter).toBeCalledTimes(0); - expect(mockMonitorReset).toBeCalledTimes(1); + expect(eventLoopDelaysMonitor.reset).toBeCalledTimes(1); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.ts index 246d88496a158..ba4e12a7bfced 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/track_threshold.ts @@ -17,7 +17,7 @@ import { MONITOR_EVENT_LOOP_WARN_THRESHOLD, ONE_MILLISECOND_AS_NANOSECONDS, } from './constants'; -import { EventLoopDelaysCollector } from './event_loop_delays'; +import type { EventLoopDelaysMonitor } from '../../../../../core/server'; /** * The monitoring of the event loop starts immediately. @@ -29,6 +29,7 @@ export function startTrackingEventLoopDelaysThreshold( eventLoopCounter: UsageCounter, logger: Logger, stopMonitoringEventLoop$: Observable, + eventLoopDelaysMonitor: EventLoopDelaysMonitor, configs: { warnThreshold?: number; collectionStartDelay?: number; @@ -41,14 +42,13 @@ export function startTrackingEventLoopDelaysThreshold( collectionInterval = MONITOR_EVENT_LOOP_THRESHOLD_INTERVAL, } = configs; - const eventLoopDelaysCollector = new EventLoopDelaysCollector(); timer(collectionStartDelay, collectionInterval) .pipe( takeUntil(stopMonitoringEventLoop$), - finalize(() => eventLoopDelaysCollector.stop()) + finalize(() => eventLoopDelaysMonitor.stop()) ) .subscribe(async () => { - const { mean } = eventLoopDelaysCollector.collect(); + const { mean } = eventLoopDelaysMonitor.collect(); const meanDurationMs = moment .duration(mean / ONE_MILLISECOND_AS_NANOSECONDS) .asMilliseconds(); @@ -64,6 +64,6 @@ export function startTrackingEventLoopDelaysThreshold( }); } - eventLoopDelaysCollector.reset(); + eventLoopDelaysMonitor.reset(); }); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/index.test.ts.snap deleted file mode 100644 index 678237ffb6ea2..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`telemetry_ops_stats should return something when there is a metric 1`] = ` -Object { - "concurrent_connections": 20, - "os": Object { - "load": Object { - "15m": 3, - "1m": 0.5, - "5m": 1, - }, - "memory": Object { - "free_in_bytes": 10, - "total_in_bytes": 10, - "used_in_bytes": 10, - }, - "platform": "darwin", - "platformRelease": "test", - "uptime_in_millis": 1000, - }, - "process": Object { - "event_loop_delay": 10, - "memory": Object { - "heap": Object { - "size_limit": 0, - "total_in_bytes": 0, - "used_in_bytes": 0, - }, - "resident_set_size_in_bytes": 0, - }, - "uptime_in_millis": 1000, - }, - "requests": Object { - "disconnects": 10, - "total": 100, - }, - "response_times": Object { - "average": 100, - "max": 200, - }, - "timestamp": Any, -} -`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap new file mode 100644 index 0000000000000..f962eca858199 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`telemetry_ops_stats should return something when there is a metric 1`] = ` +Object { + "concurrent_connections": 1, + "os": Object { + "load": Object { + "15m": 1, + "1m": 1, + "5m": 1, + }, + "memory": Object { + "free_in_bytes": 1, + "total_in_bytes": 1, + "used_in_bytes": 1, + }, + "platform": "darwin", + "platformRelease": "test", + "uptime_in_millis": 1, + }, + "process": Object { + "event_loop_delay": 1, + "event_loop_delay_histogram": Any, + "memory": Object { + "heap": Object { + "size_limit": 1, + "total_in_bytes": 1, + "used_in_bytes": 1, + }, + "resident_set_size_in_bytes": 1, + }, + "uptime_in_millis": 1, + }, + "processes": Array [ + Object { + "event_loop_delay": 1, + "event_loop_delay_histogram": Any, + "memory": Object { + "heap": Object { + "size_limit": 1, + "total_in_bytes": 1, + "used_in_bytes": 1, + }, + "resident_set_size_in_bytes": 1, + }, + "uptime_in_millis": 1, + }, + ], + "requests": Object { + "disconnects": 1, + "total": 1, + }, + "response_times": Object { + "average": 1, + "max": 1, + }, + "timestamp": Any, +} +`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.test.ts similarity index 50% rename from src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.test.ts index dfd6a93b7ea18..54a7d3e220242 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.test.ts @@ -7,15 +7,16 @@ */ import { Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { Collector, createUsageCollectionSetupMock, createCollectorFetchContextMock, } from '../../../../usage_collection/server/mocks'; -import { registerOpsStatsCollector } from './'; +import { registerOpsStatsCollector } from '.'; import { OpsMetrics } from '../../../../../core/server'; -import { loggingSystemMock } from '../../../../../core/server/mocks'; +import { loggingSystemMock, metricsServiceMock } from '../../../../../core/server/mocks'; const logger = loggingSystemMock.createLogger(); @@ -23,6 +24,8 @@ describe('telemetry_ops_stats', () => { let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); + const metricsServiceSetupMock = metricsServiceMock.createInternalSetupContract(); + usageCollectionMock.makeStatsCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeStatsCollector(config); @@ -31,45 +34,6 @@ describe('telemetry_ops_stats', () => { const metrics$ = new Subject(); const mockedFetchContext = createCollectorFetchContextMock(); - const metric: OpsMetrics = { - collected_at: new Date('2020-01-01 01:00:00'), - process: { - memory: { - heap: { - total_in_bytes: 0, - used_in_bytes: 0, - size_limit: 0, - }, - resident_set_size_in_bytes: 0, - }, - event_loop_delay: 10, - pid: 10, - uptime_in_millis: 1000, - }, - os: { - platform: 'darwin', - platformRelease: 'test', - load: { - '1m': 0.5, - '5m': 1, - '15m': 3, - }, - memory: { - total_in_bytes: 10, - free_in_bytes: 10, - used_in_bytes: 10, - }, - uptime_in_millis: 1000, - }, - response_times: { avg_in_millis: 100, max_in_millis: 200 }, - requests: { - disconnects: 10, - total: 100, - statusCodes: { 200: 100 }, - }, - concurrent_connections: 20, - }; - beforeAll(() => registerOpsStatsCollector(usageCollectionMock, metrics$)); afterAll(() => jest.clearAllTimers()); @@ -83,45 +47,18 @@ describe('telemetry_ops_stats', () => { }); test('should return something when there is a metric', async () => { - metrics$.next(metric); + const opsMetrics = await metricsServiceSetupMock.getOpsMetrics$().pipe(take(1)).toPromise(); + metrics$.next(opsMetrics); expect(collector.isReady()).toBe(true); expect(await collector.fetch(mockedFetchContext)).toMatchSnapshot({ - concurrent_connections: 20, - os: { - load: { - '15m': 3, - '1m': 0.5, - '5m': 1, - }, - memory: { - free_in_bytes: 10, - total_in_bytes: 10, - used_in_bytes: 10, - }, - platform: 'darwin', - platformRelease: 'test', - uptime_in_millis: 1000, - }, process: { - event_loop_delay: 10, - memory: { - heap: { - size_limit: 0, - total_in_bytes: 0, - used_in_bytes: 0, - }, - resident_set_size_in_bytes: 0, - }, - uptime_in_millis: 1000, - }, - requests: { - disconnects: 10, - total: 100, - }, - response_times: { - average: 100, - max: 200, + event_loop_delay_histogram: expect.any(Object), }, + processes: [ + { + event_loop_delay_histogram: expect.any(Object), + }, + ], timestamp: expect.any(String), }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts index 7959b67b4f468..ca7cca2a9de8c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts @@ -33,6 +33,11 @@ export function getOpsStatsCollector( // Ensure we only include the same data that Metricbeat collection would get // @ts-expect-error delete metrics.process.pid; + for (const process of metrics.processes) { + // @ts-expect-error + delete process.pid; + } + const responseTimes = { average: metrics.response_times.avg_in_millis, max: metrics.response_times.max_in_millis, diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 275dcc761125e..07a70dfd56fb4 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -21,7 +21,7 @@ import type { Logger, CoreUsageDataStart, } from 'src/core/server'; -import { SavedObjectsClient } from '../../../core/server'; +import { SavedObjectsClient, EventLoopDelaysMonitor } from '../../../core/server'; import { startTrackingEventLoopDelaysUsage, startTrackingEventLoopDelaysThreshold, @@ -56,6 +56,7 @@ type SavedObjectsRegisterType = SavedObjectsServiceSetup['registerType']; export class KibanaUsageCollectionPlugin implements Plugin { private readonly logger: Logger; private readonly legacyConfig$: Observable; + private readonly instanceUuid: string; private savedObjectsClient?: ISavedObjectsRepository; private uiSettingsClient?: IUiSettingsClient; private metric$: Subject; @@ -68,6 +69,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.metric$ = new Subject(); this.pluginStop$ = new Subject(); + this.instanceUuid = initializerContext.env.instanceUuid; } public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { @@ -93,11 +95,17 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); core.metrics.getOpsMetrics$().subscribe(this.metric$); this.coreUsageData = core.coreUsageData; - startTrackingEventLoopDelaysUsage(this.savedObjectsClient, this.pluginStop$.asObservable()); + startTrackingEventLoopDelaysUsage( + this.savedObjectsClient, + this.instanceUuid, + this.pluginStop$.asObservable(), + new EventLoopDelaysMonitor() + ); startTrackingEventLoopDelaysThreshold( this.eventLoopUsageCounter, this.logger, - this.pluginStop$.asObservable() + this.pluginStop$.asObservable(), + new EventLoopDelaysMonitor() ); } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 25c2f37ea04c7..666ba04654e15 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -6881,6 +6881,12 @@ "description": "The process id of the monitored kibana instance." } }, + "instanceUuid": { + "type": "keyword", + "_meta": { + "description": "The uuid of the kibana instance." + } + }, "fromTimestamp": { "type": "date", "_meta": { From 138371e50c2efeeb9a2e5d32caa27e562577c703 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 14 Sep 2021 15:55:30 +0200 Subject: [PATCH 19/39] Add deprecation warning when unknown SO types are present (#111268) * Add deprecation warning when unknown types are present * fix and add service tests * remove export * plug deprecation route * add integration test for new route * add unit test for getIndexForType * add unit tests * improve deprecation messages * add FTR test * fix things due to merge * change the name of the deprecation provider * improve message * improve message again --- .../deprecations/deprecation_factory.ts | 35 ++ .../saved_objects/deprecations/index.ts | 10 + .../unknown_object_types.test.mocks.ts | 13 + .../deprecations/unknown_object_types.test.ts | 165 ++++++ .../deprecations/unknown_object_types.ts | 172 ++++++ .../deprecations/delete_unknown_types.ts | 45 ++ .../routes/deprecations/index.ts | 9 + src/core/server/saved_objects/routes/index.ts | 5 + .../delete_unknown_types.test.ts | 93 +++ .../saved_objects_service.test.ts | 28 + .../saved_objects/saved_objects_service.ts | 20 +- .../service/lib/get_index_for_type.test.ts | 80 +++ .../service/lib/get_index_for_type.ts | 36 ++ .../server/saved_objects/service/lib/index.ts | 2 + .../saved_objects/service/lib/repository.ts | 18 +- src/core/server/server.ts | 4 +- .../saved_objects/delete_unknown_types.ts | 125 +++++ .../apis/saved_objects/index.ts | 1 + .../delete_unknown_types/data.json | 182 ++++++ .../delete_unknown_types/mappings.json | 530 ++++++++++++++++++ 20 files changed, 1561 insertions(+), 12 deletions(-) create mode 100644 src/core/server/saved_objects/deprecations/deprecation_factory.ts create mode 100644 src/core/server/saved_objects/deprecations/index.ts create mode 100644 src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts create mode 100644 src/core/server/saved_objects/deprecations/unknown_object_types.test.ts create mode 100644 src/core/server/saved_objects/deprecations/unknown_object_types.ts create mode 100644 src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts create mode 100644 src/core/server/saved_objects/routes/deprecations/index.ts create mode 100644 src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts create mode 100644 src/core/server/saved_objects/service/lib/get_index_for_type.test.ts create mode 100644 src/core/server/saved_objects/service/lib/get_index_for_type.ts create mode 100644 test/api_integration/apis/saved_objects/delete_unknown_types.ts create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json diff --git a/src/core/server/saved_objects/deprecations/deprecation_factory.ts b/src/core/server/saved_objects/deprecations/deprecation_factory.ts new file mode 100644 index 0000000000000..670b43bfa7c77 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/deprecation_factory.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RegisterDeprecationsConfig } from '../../deprecations'; +import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import type { SavedObjectConfig } from '../saved_objects_config'; +import type { KibanaConfigType } from '../../kibana_config'; +import { getUnknownTypesDeprecations } from './unknown_object_types'; + +interface GetDeprecationProviderOptions { + typeRegistry: ISavedObjectTypeRegistry; + savedObjectsConfig: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +} + +export const getSavedObjectsDeprecationsProvider = ( + config: GetDeprecationProviderOptions +): RegisterDeprecationsConfig => { + return { + getDeprecations: async (context) => { + return [ + ...(await getUnknownTypesDeprecations({ + ...config, + esClient: context.esClient, + })), + ]; + }, + }; +}; diff --git a/src/core/server/saved_objects/deprecations/index.ts b/src/core/server/saved_objects/deprecations/index.ts new file mode 100644 index 0000000000000..5cf1590ad43d2 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getSavedObjectsDeprecationsProvider } from './deprecation_factory'; +export { deleteUnknownTypeObjects } from './unknown_object_types'; diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts new file mode 100644 index 0000000000000..312204ad77846 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const getIndexForTypeMock = jest.fn(); + +jest.doMock('../service/lib/get_index_for_type', () => ({ + getIndexForType: getIndexForTypeMock, +})); diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts new file mode 100644 index 0000000000000..d7ea73456e236 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getIndexForTypeMock } from './unknown_object_types.test.mocks'; + +import { estypes } from '@elastic/elasticsearch'; +import { deleteUnknownTypeObjects, getUnknownTypesDeprecations } from './unknown_object_types'; +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; +import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; +import type { KibanaConfigType } from '../../kibana_config'; +import type { SavedObjectConfig } from '../saved_objects_config'; +import { SavedObjectsType } from 'kibana/server'; + +const createSearchResponse = (count: number): estypes.SearchResponse => { + return { + hits: { + total: count, + max_score: 0, + hits: new Array(count).fill({}), + }, + } as estypes.SearchResponse; +}; + +describe('unknown saved object types deprecation', () => { + const kibanaVersion = '8.0.0'; + + let typeRegistry: ReturnType; + let esClient: ReturnType; + let kibanaConfig: KibanaConfigType; + let savedObjectsConfig: SavedObjectConfig; + + beforeEach(() => { + typeRegistry = typeRegistryMock.create(); + esClient = elasticsearchClientMock.createScopedClusterClient(); + + typeRegistry.getAllTypes.mockReturnValue([ + { name: 'foo' }, + { name: 'bar' }, + ] as SavedObjectsType[]); + getIndexForTypeMock.mockImplementation(({ type }: { type: string }) => `${type}-index`); + + kibanaConfig = { + index: '.kibana', + enabled: true, + }; + + savedObjectsConfig = { + migration: { + enableV2: true, + }, + } as SavedObjectConfig; + }); + + afterEach(() => { + getIndexForTypeMock.mockReset(); + }); + + describe('getUnknownTypesDeprecations', () => { + beforeEach(() => { + esClient.asInternalUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(createSearchResponse(0)) + ); + }); + + it('calls `esClient.asInternalUser.search` with the correct parameters', async () => { + await getUnknownTypesDeprecations({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(esClient.asInternalUser.search).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.search).toHaveBeenCalledWith({ + index: ['foo-index', 'bar-index'], + body: { + size: 10000, + query: { + bool: { + must_not: [{ term: { type: 'foo' } }, { term: { type: 'bar' } }], + }, + }, + }, + }); + }); + + it('returns no deprecation if no unknown type docs are found', async () => { + esClient.asInternalUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(createSearchResponse(0)) + ); + + const deprecations = await getUnknownTypesDeprecations({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(deprecations.length).toEqual(0); + }); + + it('returns a deprecation if any unknown type docs are found', async () => { + esClient.asInternalUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(createSearchResponse(1)) + ); + + const deprecations = await getUnknownTypesDeprecations({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(deprecations.length).toEqual(1); + expect(deprecations[0]).toEqual({ + title: expect.any(String), + message: expect.any(String), + level: 'critical', + requireRestart: false, + deprecationType: undefined, + correctiveActions: { + manualSteps: expect.any(Array), + api: { + path: '/internal/saved_objects/deprecations/_delete_unknown_types', + method: 'POST', + body: {}, + }, + }, + }); + }); + }); + + describe('deleteUnknownTypeObjects', () => { + it('calls `esClient.asInternalUser.search` with the correct parameters', async () => { + await deleteUnknownTypeObjects({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(esClient.asInternalUser.deleteByQuery).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.deleteByQuery).toHaveBeenCalledWith({ + index: ['foo-index', 'bar-index'], + wait_for_completion: false, + body: { + query: { + bool: { + must_not: [{ term: { type: 'foo' } }, { term: { type: 'bar' } }], + }, + }, + }, + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts new file mode 100644 index 0000000000000..c966e621ca605 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { estypes } from '@elastic/elasticsearch'; +import { i18n } from '@kbn/i18n'; +import type { DeprecationsDetails } from '../../deprecations'; +import { IScopedClusterClient } from '../../elasticsearch'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsRawDocSource } from '../serialization'; +import type { KibanaConfigType } from '../../kibana_config'; +import type { SavedObjectConfig } from '../saved_objects_config'; +import { getIndexForType } from '../service/lib'; + +interface UnknownTypesDeprecationOptions { + typeRegistry: ISavedObjectTypeRegistry; + esClient: IScopedClusterClient; + kibanaConfig: KibanaConfigType; + savedObjectsConfig: SavedObjectConfig; + kibanaVersion: string; +} + +const getKnownTypes = (typeRegistry: ISavedObjectTypeRegistry) => + typeRegistry.getAllTypes().map((type) => type.name); + +const getTargetIndices = ({ + types, + typeRegistry, + kibanaVersion, + kibanaConfig, + savedObjectsConfig, +}: { + types: string[]; + typeRegistry: ISavedObjectTypeRegistry; + savedObjectsConfig: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +}) => { + return [ + ...new Set( + types.map((type) => + getIndexForType({ + type, + typeRegistry, + migV2Enabled: savedObjectsConfig.migration.enableV2, + kibanaVersion, + defaultIndex: kibanaConfig.index, + }) + ) + ), + ]; +}; + +const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContainer => { + return { + bool: { + must_not: knownTypes.map((type) => ({ + term: { type }, + })), + }, + }; +}; + +const getUnknownSavedObjects = async ({ + typeRegistry, + esClient, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, +}: UnknownTypesDeprecationOptions) => { + const knownTypes = getKnownTypes(typeRegistry); + const targetIndices = getTargetIndices({ + types: knownTypes, + typeRegistry, + kibanaConfig, + kibanaVersion, + savedObjectsConfig, + }); + const query = getUnknownTypesQuery(knownTypes); + + const { body } = await esClient.asInternalUser.search({ + index: targetIndices, + body: { + size: 10000, + query, + }, + }); + const { hits: unknownDocs } = body.hits; + + return unknownDocs.map((doc) => ({ id: doc._id, type: doc._source?.type ?? 'unknown' })); +}; + +export const getUnknownTypesDeprecations = async ( + options: UnknownTypesDeprecationOptions +): Promise => { + const deprecations: DeprecationsDetails[] = []; + const unknownDocs = await getUnknownSavedObjects(options); + if (unknownDocs.length) { + deprecations.push({ + title: i18n.translate('core.savedObjects.deprecations.unknownTypes.title', { + defaultMessage: 'Saved objects with unknown types are present in Kibana system indices', + }), + message: i18n.translate('core.savedObjects.deprecations.unknownTypes.message', { + defaultMessage: + '{objectCount, plural, one {# object} other {# objects}} with unknown types {objectCount, plural, one {was} other {were}} found in Kibana system indices. ' + + 'Upgrading with unknown savedObject types is no longer supported. ' + + `To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the Kibana indices`, + values: { + objectCount: unknownDocs.length, + }, + }), + level: 'critical', + requireRestart: false, + deprecationType: undefined, // not config nor feature... + correctiveActions: { + manualSteps: [ + i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.1', { + defaultMessage: 'Enable disabled plugins then restart Kibana.', + }), + i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.2', { + defaultMessage: + 'If no plugins are disabled, or if enabling them does not fix the issue, delete the documents.', + }), + ], + api: { + path: '/internal/saved_objects/deprecations/_delete_unknown_types', + method: 'POST', + body: {}, + }, + }, + }); + } + return deprecations; +}; + +interface DeleteUnknownTypesOptions { + typeRegistry: ISavedObjectTypeRegistry; + esClient: IScopedClusterClient; + kibanaConfig: KibanaConfigType; + savedObjectsConfig: SavedObjectConfig; + kibanaVersion: string; +} + +export const deleteUnknownTypeObjects = async ({ + esClient, + typeRegistry, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, +}: DeleteUnknownTypesOptions) => { + const knownTypes = getKnownTypes(typeRegistry); + const targetIndices = getTargetIndices({ + types: knownTypes, + typeRegistry, + kibanaConfig, + kibanaVersion, + savedObjectsConfig, + }); + const query = getUnknownTypesQuery(knownTypes); + + await esClient.asInternalUser.deleteByQuery({ + index: targetIndices, + wait_for_completion: false, + body: { + query, + }, + }); +}; diff --git a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts new file mode 100644 index 0000000000000..a9e1a41f01d91 --- /dev/null +++ b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IRouter } from '../../../http'; +import { catchAndReturnBoomErrors } from '../utils'; +import { deleteUnknownTypeObjects } from '../../deprecations'; +import { SavedObjectConfig } from '../../saved_objects_config'; +import { KibanaConfigType } from '../../../kibana_config'; + +interface RouteDependencies { + config: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +} + +export const registerDeleteUnknownTypesRoute = ( + router: IRouter, + { config, kibanaConfig, kibanaVersion }: RouteDependencies +) => { + router.post( + { + path: '/deprecations/_delete_unknown_types', + validate: false, + }, + catchAndReturnBoomErrors(async (context, req, res) => { + await deleteUnknownTypeObjects({ + esClient: context.core.elasticsearch.client, + typeRegistry: context.core.savedObjects.typeRegistry, + savedObjectsConfig: config, + kibanaConfig, + kibanaVersion, + }); + return res.ok({ + body: { + success: true, + }, + }); + }) + ); +}; diff --git a/src/core/server/saved_objects/routes/deprecations/index.ts b/src/core/server/saved_objects/routes/deprecations/index.ts new file mode 100644 index 0000000000000..07e6b987d7c60 --- /dev/null +++ b/src/core/server/saved_objects/routes/deprecations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerDeleteUnknownTypesRoute } from './delete_unknown_types'; diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 8511b59a0758f..461f837480789 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -26,6 +26,8 @@ import { registerResolveImportErrorsRoute } from './resolve_import_errors'; import { registerMigrateRoute } from './migrate'; import { registerLegacyImportRoute } from './legacy_import_export/import'; import { registerLegacyExportRoute } from './legacy_import_export/export'; +import { registerDeleteUnknownTypesRoute } from './deprecations'; +import { KibanaConfigType } from '../../kibana_config'; export function registerRoutes({ http, @@ -34,6 +36,7 @@ export function registerRoutes({ config, migratorPromise, kibanaVersion, + kibanaConfig, }: { http: InternalHttpServiceSetup; coreUsageData: InternalCoreUsageDataSetup; @@ -41,6 +44,7 @@ export function registerRoutes({ config: SavedObjectConfig; migratorPromise: Promise; kibanaVersion: string; + kibanaConfig: KibanaConfigType; }) { const router = http.createRouter('/api/saved_objects/'); @@ -68,4 +72,5 @@ export function registerRoutes({ const internalRouter = http.createRouter('/internal/saved_objects/'); registerMigrateRoute(internalRouter, migratorPromise); + registerDeleteUnknownTypesRoute(internalRouter, { config, kibanaConfig, kibanaVersion }); } diff --git a/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts new file mode 100644 index 0000000000000..fef2b2d5870e0 --- /dev/null +++ b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { registerDeleteUnknownTypesRoute } from '../deprecations'; +import { elasticsearchServiceMock } from '../../../../../core/server/elasticsearch/elasticsearch_service.mock'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { setupServer } from '../test_utils'; +import { KibanaConfigType } from '../../../kibana_config'; +import { SavedObjectConfig } from '../../saved_objects_config'; +import { SavedObjectsType } from 'kibana/server'; + +type SetupServerReturn = UnwrapPromise>; + +describe('POST /internal/saved_objects/deprecations/_delete_unknown_types', () => { + const kibanaVersion = '8.0.0'; + const kibanaConfig: KibanaConfigType = { + enabled: true, + index: '.kibana', + }; + const config: SavedObjectConfig = { + maxImportExportSize: 10000, + maxImportPayloadBytes: 24000000, + migration: { + enableV2: true, + } as SavedObjectConfig['migration'], + }; + + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let handlerContext: SetupServerReturn['handlerContext']; + let typeRegistry: ReturnType; + let elasticsearchClient: ReturnType; + + beforeEach(async () => { + ({ server, httpSetup, handlerContext } = await setupServer()); + elasticsearchClient = elasticsearchServiceMock.createScopedClusterClient(); + typeRegistry = typeRegistryMock.create(); + + typeRegistry.getAllTypes.mockReturnValue([{ name: 'known-type' } as SavedObjectsType]); + typeRegistry.getIndex.mockImplementation((type) => `${type}-index`); + + handlerContext.savedObjects.typeRegistry = typeRegistry; + handlerContext.elasticsearch.client.asCurrentUser = elasticsearchClient.asCurrentUser; + handlerContext.elasticsearch.client.asInternalUser = elasticsearchClient.asInternalUser; + + const router = httpSetup.createRouter('/internal/saved_objects/'); + registerDeleteUnknownTypesRoute(router, { + kibanaVersion, + kibanaConfig, + config, + }); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('formats successful response', async () => { + const result = await supertest(httpSetup.server.listener) + .post('/internal/saved_objects/deprecations/_delete_unknown_types') + .expect(200); + + expect(result.body).toEqual({ success: true }); + }); + + it('calls upon esClient.deleteByQuery', async () => { + await supertest(httpSetup.server.listener) + .post('/internal/saved_objects/deprecations/_delete_unknown_types') + .expect(200); + + expect(elasticsearchClient.asInternalUser.deleteByQuery).toHaveBeenCalledTimes(1); + expect(elasticsearchClient.asInternalUser.deleteByQuery).toHaveBeenCalledWith({ + index: ['known-type-index_8.0.0'], + wait_for_completion: false, + body: { + query: { + bool: { + must_not: expect.any(Array), + }, + }, + }, + }); + }); +}); 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 135996f49cea4..6477d1a3dfbeb 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -20,17 +20,26 @@ import { Env } from '../config'; import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; +import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { httpServerMock } from '../http/http_server.mocks'; import { SavedObjectsClientFactoryProvider } from './service/lib'; import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; import { SavedObjectsRepository } from './service/lib/repository'; import { registerCoreObjectTypes } from './object_types'; +import { getSavedObjectsDeprecationsProvider } from './deprecations'; jest.mock('./service/lib/repository'); jest.mock('./object_types'); +jest.mock('./deprecations'); describe('SavedObjectsService', () => { + let deprecationsSetup: ReturnType; + + beforeEach(() => { + deprecationsSetup = deprecationsServiceMock.createInternalSetupContract(); + }); + const createCoreContext = ({ skipMigration = true, env, @@ -53,6 +62,7 @@ describe('SavedObjectsService', () => { return { http: httpServiceMock.createInternalSetupContract(), elasticsearch: elasticsearchMock, + deprecations: deprecationsSetup, coreUsageData: coreUsageDataServiceMock.createSetupContract(), }; }; @@ -79,6 +89,24 @@ describe('SavedObjectsService', () => { expect(mockedRegisterCoreObjectTypes).toHaveBeenCalledTimes(1); }); + it('register the deprecation provider', async () => { + const coreContext = createCoreContext(); + const soService = new SavedObjectsService(coreContext); + + const mockRegistry = deprecationsServiceMock.createSetupContract(); + deprecationsSetup.getRegistry.mockReturnValue(mockRegistry); + + const deprecations = Symbol('deprecations'); + const mockedGetSavedObjectsDeprecationsProvider = getSavedObjectsDeprecationsProvider as jest.Mock; + mockedGetSavedObjectsDeprecationsProvider.mockReturnValue(deprecations); + await soService.setup(createSetupDeps()); + + expect(deprecationsSetup.getRegistry).toHaveBeenCalledTimes(1); + expect(deprecationsSetup.getRegistry).toHaveBeenCalledWith('savedObjects'); + expect(mockRegistry.registerDeprecations).toHaveBeenCalledTimes(1); + expect(mockRegistry.registerDeprecations).toHaveBeenCalledWith(deprecations); + }); + describe('#setClientFactoryProvider', () => { it('registers the factory to the clientProvider', async () => { const coreContext = createCoreContext(); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b298396a2aee0..ee56744249c5b 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -22,6 +22,7 @@ import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart, } from '../elasticsearch'; +import { InternalDeprecationsServiceSetup } from '../deprecations'; import { KibanaConfigType } from '../kibana_config'; import { SavedObjectsConfigType, @@ -44,6 +45,7 @@ import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; import { registerCoreObjectTypes } from './object_types'; +import { getSavedObjectsDeprecationsProvider } from './deprecations'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -251,6 +253,7 @@ export interface SavedObjectsSetupDeps { http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; coreUsageData: InternalCoreUsageDataSetup; + deprecations: InternalDeprecationsServiceSetup; } interface WrappedClientFactoryWrapper { @@ -286,7 +289,7 @@ export class SavedObjectsService this.logger.debug('Setting up SavedObjects service'); this.setupDeps = setupDeps; - const { http, elasticsearch, coreUsageData } = setupDeps; + const { http, elasticsearch, coreUsageData, deprecations } = setupDeps; const savedObjectsConfig = await this.coreContext.configService .atPath('savedObjects') @@ -298,6 +301,20 @@ export class SavedObjectsService .toPromise(); this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig); + const kibanaConfig = await this.coreContext.configService + .atPath('kibana') + .pipe(first()) + .toPromise(); + + deprecations.getRegistry('savedObjects').registerDeprecations( + getSavedObjectsDeprecationsProvider({ + kibanaConfig, + savedObjectsConfig: this.config, + kibanaVersion: this.coreContext.env.packageInfo.version, + typeRegistry: this.typeRegistry, + }) + ); + coreUsageData.registerType(this.typeRegistry); registerRoutes({ @@ -306,6 +323,7 @@ export class SavedObjectsService logger: this.logger, config: this.config, migratorPromise: this.migrator$.pipe(first()).toPromise(), + kibanaConfig, kibanaVersion: this.coreContext.env.packageInfo.version, }); diff --git a/src/core/server/saved_objects/service/lib/get_index_for_type.test.ts b/src/core/server/saved_objects/service/lib/get_index_for_type.test.ts new file mode 100644 index 0000000000000..fa065b02b8050 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/get_index_for_type.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getIndexForType } from './get_index_for_type'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; + +describe('getIndexForType', () => { + const kibanaVersion = '8.0.0'; + const defaultIndex = '.kibana'; + let typeRegistry: ReturnType; + + beforeEach(() => { + typeRegistry = typeRegistryMock.create(); + }); + + describe('when migV2 is enabled', () => { + const migV2Enabled = true; + + it('returns the correct index for a type specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => `.${type}-index`); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.foo-index_8.0.0'); + }); + + it('returns the correct index for a type not specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => undefined); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.kibana_8.0.0'); + }); + }); + + describe('when migV2 is disabled', () => { + const migV2Enabled = false; + + it('returns the correct index for a type specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => `.${type}-index`); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.foo-index'); + }); + + it('returns the correct index for a type not specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => undefined); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.kibana'); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/get_index_for_type.ts b/src/core/server/saved_objects/service/lib/get_index_for_type.ts new file mode 100644 index 0000000000000..cef477e6dd840 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/get_index_for_type.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; + +interface GetIndexForTypeOptions { + type: string; + typeRegistry: ISavedObjectTypeRegistry; + migV2Enabled: boolean; + kibanaVersion: string; + defaultIndex: string; +} + +export const getIndexForType = ({ + type, + typeRegistry, + migV2Enabled, + defaultIndex, + kibanaVersion, +}: GetIndexForTypeOptions): string => { + // TODO migrationsV2: Remove once we remove migrations v1 + // This is a hacky, but it required the least amount of changes to + // existing code to support a migrations v2 index. Long term we would + // want to always use the type registry to resolve a type's index + // (including the default index). + if (migV2Enabled) { + return `${typeRegistry.getIndex(type) || defaultIndex}_${kibanaVersion}`; + } else { + return typeRegistry.getIndex(type) || defaultIndex; + } +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 661d04b8a0b2a..ec283f3d3741e 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -41,3 +41,5 @@ export type { SavedObjectsUpdateObjectsSpacesResponse, SavedObjectsUpdateObjectsSpacesResponseObject, } from './update_objects_spaces'; + +export { getIndexForType } from './get_index_for_type'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e49b2e413981f..c425f8c40fed1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -92,6 +92,7 @@ import { SavedObjectsUpdateObjectsSpacesObject, SavedObjectsUpdateObjectsSpacesOptions, } from './update_objects_spaces'; +import { getIndexForType } from './get_index_for_type'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -2099,16 +2100,13 @@ export class SavedObjectsRepository { * @param type - the type */ private getIndexForType(type: string) { - // TODO migrationsV2: Remove once we remove migrations v1 - // This is a hacky, but it required the least amount of changes to - // existing code to support a migrations v2 index. Long term we would - // want to always use the type registry to resolve a type's index - // (including the default index). - if (this._migrator.soMigrationsConfig.enableV2) { - return `${this._registry.getIndex(type) || this._index}_${this._migrator.kibanaVersion}`; - } else { - return this._registry.getIndex(type) || this._index; - } + return getIndexForType({ + type, + defaultIndex: this._index, + typeRegistry: this._registry, + kibanaVersion: this._migrator.kibanaVersion, + migV2Enabled: this._migrator.soMigrationsConfig.enableV2, + }); } /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index ebf3e35870421..cd133def69a67 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -233,6 +233,7 @@ export class Server { const savedObjectsSetup = await this.savedObjects.setup({ http: httpSetup, elasticsearch: elasticsearchServiceSetup, + deprecations: deprecationsSetup, coreUsageData: coreUsageDataSetup, }); @@ -303,6 +304,7 @@ export class Server { const executionContextStart = this.executionContext.start(); const elasticsearchStart = await this.elasticsearch.start(); + const deprecationsStart = this.deprecations.start(); const soStartSpan = startTransaction?.startSpan('saved_objects.migration', 'migration'); const savedObjectsStart = await this.savedObjects.start({ elasticsearch: elasticsearchStart, @@ -320,7 +322,7 @@ export class Server { savedObjects: savedObjectsStart, exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(), }); - const deprecationsStart = this.deprecations.start(); + this.status.start(); this.coreStart = { diff --git a/test/api_integration/apis/saved_objects/delete_unknown_types.ts b/test/api_integration/apis/saved_objects/delete_unknown_types.ts new file mode 100644 index 0000000000000..42caa753683e1 --- /dev/null +++ b/test/api_integration/apis/saved_objects/delete_unknown_types.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('/deprecations/_delete_unknown_types', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types' + ); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types' + ); + }); + + const fetchIndexContent = async () => { + const { body } = await es.search<{ type: string }>({ + index: '.kibana', + body: { + size: 100, + }, + }); + return body.hits.hits + .map((hit) => ({ + type: hit._source!.type, + id: hit._id, + })) + .sort((a, b) => { + return a.id > b.id ? 1 : -1; + }); + }; + + it('should return 200 with individual responses', async () => { + const beforeDelete = await fetchIndexContent(); + expect(beforeDelete).to.eql([ + { + id: 'dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357', + type: 'dashboard', + }, + { + id: 'index-pattern:8963ca30-3224-11e8-a572-ffca06da1357', + type: 'index-pattern', + }, + { + id: 'search:960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + }, + { + id: 'space:default', + type: 'space', + }, + { + id: 'unknown-shareable-doc', + type: 'unknown-shareable-type', + }, + { + id: 'unknown-type:unknown-doc', + type: 'unknown-type', + }, + { + id: 'visualization:a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + }, + ]); + + await supertest + .post(`/internal/saved_objects/deprecations/_delete_unknown_types`) + .send({}) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ success: true }); + }); + + for (let i = 0; i < 10; i++) { + const afterDelete = await fetchIndexContent(); + // we're deleting with `wait_for_completion: false` and we don't surface + // the task ID in the API, so we're forced to use pooling for the FTR tests + if (afterDelete.map((obj) => obj.type).includes('unknown-type') && i < 10) { + await delay(1000); + continue; + } + expect(afterDelete).to.eql([ + { + id: 'dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357', + type: 'dashboard', + }, + { + id: 'index-pattern:8963ca30-3224-11e8-a572-ffca06da1357', + type: 'index-pattern', + }, + { + id: 'search:960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + }, + { + id: 'space:default', + type: 'space', + }, + { + id: 'visualization:a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + }, + ]); + break; + } + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index 2af1df01c0f92..12189bce302b8 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./resolve')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./delete_unknown_types')); }); } diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json new file mode 100644 index 0000000000000..3d6ecd160db00 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json @@ -0,0 +1,182 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "saved_objects*" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:960372e0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "8963ca30-3224-11e8-a572-ffca06da1357", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "_source" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "_score", + "desc" + ] + ], + "title": "OneRecord", + "version": 1 + }, + "type": "search", + "updated_at": "2018-03-28T01:08:55.182Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "960372e0-3224-11e8-a572-ffca06da1357", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-03-28T01:09:18.936Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + }, + "savedSearchRefName": "search_0", + "title": "VisualizationFromSavedSearch", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Dashboard", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "add810b0-3224-11e8-a572-ffca06da1357", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "a42c0580-3224-11e8-a572-ffca06da1357", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "unknown-type:unknown-doc", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "unknown-type": { + "foo": "bar" + }, + "migrationVersion": {}, + "references": [ + ], + "type": "unknown-type", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "unknown-shareable-doc", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "unknown-shareable-type": { + "foo": "bar" + }, + "migrationVersion": {}, + "references": [ + ], + "type": "unknown-shareable-type", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json new file mode 100644 index 0000000000000..f745e0f69c5d3 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json @@ -0,0 +1,530 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, + ".kibana": {} + }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, + "dynamic": "strict", + "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "unknown-type": { + "dynamic": "false", + "properties": { + "foo": { + "type": "keyword" + } + } + }, + "unknown-shareable-type": { + "dynamic": "false", + "properties": { + "foo": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" + }, + "sourceId": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "server": { + "dynamic": "false", + "type": "object" + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } + } + } +} From 9762da497d4ed65805ade48f0847181b571296d5 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 14 Sep 2021 15:31:08 +0100 Subject: [PATCH 20/39] [QA][SO INFO SVC] Add json only option (#112061) --- .../services/saved_object_info/index.ts | 13 ++++--- .../services/saved_object_info/utils.ts | 39 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/test/common/services/saved_object_info/index.ts b/test/common/services/saved_object_info/index.ts index a8e777e4e3bb8..41367694373f3 100644 --- a/test/common/services/saved_object_info/index.ts +++ b/test/common/services/saved_object_info/index.ts @@ -8,7 +8,7 @@ import { run } from '@kbn/dev-utils'; import { pipe } from 'fp-ts/function'; -import { payload, noop, areValid, print, expectedFlags } from './utils'; +import { payload, noop, areValid, print, expectedFlags, format } from './utils'; import { types } from './saved_object_info'; export { SavedObjectInfoService } from './saved_object_info'; @@ -16,12 +16,15 @@ export { SavedObjectInfoService } from './saved_object_info'; export const runSavedObjInfoSvc = () => run( async ({ flags, log }) => { - const printWith = print(log); + const justJson: boolean = !!flags.json; - const getAndFormatAndPrint = async () => - pipe(await types(flags.esUrl as string)(), payload, printWith()); + const resolveDotKibana = async () => await types(flags.esUrl as string)(); - return areValid(flags) ? getAndFormatAndPrint() : noop(); + return areValid(flags) + ? justJson + ? pipe(await resolveDotKibana(), JSON.stringify.bind(null), log.write.bind(log)) + : pipe(await resolveDotKibana(), format, payload, print(log)()) + : noop(); }, { description: ` diff --git a/test/common/services/saved_object_info/utils.ts b/test/common/services/saved_object_info/utils.ts index 43ec565051744..658803560eb6d 100644 --- a/test/common/services/saved_object_info/utils.ts +++ b/test/common/services/saved_object_info/utils.ts @@ -9,13 +9,22 @@ import { inspect } from 'util'; import { createFlagError, ToolingLog } from '@kbn/dev-utils'; -export const format = (obj: unknown) => - inspect(obj, { - compact: false, - depth: 99, - breakLength: 80, - sorted: true, - }); +interface ResolvedPayload { + xs: any; + count: number; +} + +export const format = (obj: any): ResolvedPayload => { + return { + xs: inspect(obj, { + compact: false, + depth: 99, + breakLength: 80, + sorted: true, + }), + count: obj.length, + }; +}; export const noop = () => {}; @@ -24,20 +33,22 @@ export const areValid = (flags: any) => { return true; }; -// @ts-ignore -export const print = (log: ToolingLog) => (msg: string | null = null) => ({ xs, count }) => - log.success(`\n### Saved Object Types ${msg || 'Count: ' + count}\n${xs}`); +export const print = (log: ToolingLog) => (msg: string | null = null) => ({ + xs, + count, +}: ResolvedPayload) => log.write(`\n### Saved Object Types ${msg || 'Count: ' + count}\n${xs}`); export const expectedFlags = () => ({ string: ['esUrl'], - boolean: ['soTypes'], + boolean: ['soTypes', 'json'], help: ` --esUrl Required, tells the app which url to point to --soTypes Not Required, tells the svc to show the types within the .kibana index +--json Not Required, tells the svc to show the types, with only json output. Useful for piping into jq `, }); -export const payload = (xs: any) => ({ - xs: format(xs), - count: xs.length, +export const payload = ({ xs, count }: ResolvedPayload) => ({ + xs, + count, }); From b35b642fb7ac9325a337657ea3999686524c9e6f Mon Sep 17 00:00:00 2001 From: juliaElastic <90178898+juliaElastic@users.noreply.github.com> Date: Tue, 14 Sep 2021 16:39:41 +0200 Subject: [PATCH 21/39] [Fleet] Cosmetic fixes (#111725) * [Fleet] Cosmetic fixes Made Add integration button filled * [Fleet] Cosmetic fixes Filled button fixes * [Fleet] Cosmetic fixes 3. Assign policy button * [Fleet] Cosmetic fixes 4. Duplicate policy * [Fleet] Cosmetic fixes 5. full stop * [Fleet] Cosmetic fixes 6. enrollment token name * [Fleet] Cosmetic fixes 2. auth settings * [Fleet] Cosmetic fixes review fixes * [Fleet] Cosmetic fixes Responsive fix fleet broken icons * [Fleet] Cosmetic fixes 8. data streams table layout auto * [Fleet] Cosmetic fixes responsive 2. fixed icon overlap * [Fleet] Cosmetic fixes reduced empty space in header * [Fleet] Cosmetic fixes fix add integration form * improve css selector * moved css to styled component --- .../layouts/default/default_page_title.tsx | 2 +- .../agent_policy/components/actions_menu.tsx | 2 +- .../components/agent_policy_copy_provider.tsx | 12 ++++----- .../step_define_package_policy.tsx | 15 +++++++++-- .../package_policies_table.tsx | 1 + .../components/actions_menu.tsx | 2 +- .../agent_reassign_policy_modal/index.tsx | 2 +- .../enrollment_token_list_page/index.tsx | 2 +- .../sections/data_stream/list_page/index.tsx | 1 + .../screens/detail/components/icon_panel.tsx | 27 ++++++------------- ...advanced_agent_authentication_settings.tsx | 2 +- .../agent_policy_package_badges.tsx | 2 +- .../fleet/public/components/header.tsx | 6 +++++ .../components/new_enrollment_key_modal.tsx | 9 +++++-- 14 files changed, 49 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx index e525a059b7837..3d0a7717d1e1a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx @@ -30,7 +30,7 @@ export const DefaultPageTitle: FunctionComponent = () => {

diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index d8f13da64257b..08b0507f7c621 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -101,7 +101,7 @@ export const AgentPolicyActionMenu = memo<{ > , ]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx index ef623d30b8847..68f05624a8664 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -34,7 +34,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr onSuccess = () => undefined ) => { if (!agentPolicyToCopy) { - throw new Error('No agent policy specified to copy'); + throw new Error('No agent policy specified to duplicate'); } setIsModalOpen(true); setAgentPolicy(agentPolicyToCopy); @@ -63,7 +63,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.fleet.copyAgentPolicy.successNotificationTitle', { - defaultMessage: 'Agent policy copied', + defaultMessage: 'Agent policy duplicated', }) ); if (onSuccessCallback.current) { @@ -72,7 +72,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } else { notifications.toasts.addDanger( i18n.translate('xpack.fleet.copyAgentPolicy.failureNotificationTitle', { - defaultMessage: "Error copying agent policy '{id}'", + defaultMessage: "Error duplicating agent policy '{id}'", values: { id: agentPolicy!.id }, }) ); @@ -80,7 +80,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } catch (e) { notifications.toasts.addDanger( i18n.translate('xpack.fleet.copyAgentPolicy.fatalErrorNotificationTitle', { - defaultMessage: 'Error copying agent policy', + defaultMessage: 'Error duplicating agent policy', }) ); } @@ -98,7 +98,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr = ({ childr confirmButtonText={ } confirmButtonDisabled={isLoading || !newAgentPolicy.name.trim()} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index fde09c3dbea3a..e84831a3006f4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -20,6 +20,8 @@ import { EuiLink, } from '@elastic/eui'; +import styled from 'styled-components'; + import type { AgentPolicy, PackageInfo, @@ -35,6 +37,15 @@ import { isAdvancedVar } from './services'; import type { PackagePolicyValidationResults } from './services'; import { PackagePolicyInputVarField } from './components'; +// on smaller screens, fields should be displayed in one column +const FormGroupResponsiveFields = styled(EuiDescribedFormGroup)` + @media (max-width: 767px) { + .euiFlexGroup--responsive { + align-items: flex-start; + } + } +`; + export const StepDefinePackagePolicy: React.FunctionComponent<{ agentPolicy: AgentPolicy; packageInfo: PackageInfo; @@ -113,7 +124,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ }, [packagePolicy, agentPolicy, packageInfo, updatePackagePolicy, integrationToEnable]); return validationResults ? ( - ) : null} - + ) : ( ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index eca02096781a6..d3a6bb7561d39 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -266,6 +266,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ : [ { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx index 01627159f39bc..0e36c99432cf5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx @@ -86,7 +86,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ )} = ({ defaultMessage="Assign policy" /> } - buttonColor="danger" + buttonColor="primary" >

= () => { /> - setModalOpen(true)}> + setModalOpen(true)}> = () => { - parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; - height: 1px; - z-index: 1; -`; - const Panel = styled(EuiPanel)` padding: ${(props) => props.theme.eui.spacerSizes.xl}; - margin-bottom: -100%; + width: ${(props) => + parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; svg, img { height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; @@ -44,20 +37,16 @@ export function IconPanel({ const iconType = usePackageIconType({ packageName, integrationName, version, icons }); return ( - - - - - + + + ); } export function LoadingIconPanel() { return ( - - - - - + + + ); } diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx index 178c1716d3355..26e7fc909402e 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx @@ -179,7 +179,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ return ( <> setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)} > diff --git a/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx b/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx index 418e274022461..02518945cf7a5 100644 --- a/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx +++ b/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx @@ -91,7 +91,7 @@ export const AgentPolicyPackageBadges: React.FunctionComponent = ({ color="hollow" isDisabled={excludeFleetServer && pkg.name === FLEET_SERVER_PACKAGE} > - + props.theme.eui.euiBorderThin}; background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; + + @media (max-width: 767px) { + .euiFlexItem { + margin-bottom: 0 !important; + } + } `; const Wrapper = styled.div<{ maxWidth?: number }>` diff --git a/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx b/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx index 0752c1ab34889..362c6e6bdb061 100644 --- a/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx +++ b/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx @@ -86,13 +86,18 @@ export const NewEnrollmentTokenModal: React.FunctionComponent = ({

From ee7e832489b5be92f75005ddad7e3cafcad068f8 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 14 Sep 2021 16:23:55 +0100 Subject: [PATCH 22/39] [ML] Re-enabling skipped date nanos test (#112083) --- .../functional/apps/ml/anomaly_detection/date_nanos_job.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index 5f8d346ee4473..d351e8f7057e4 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -114,8 +114,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - // test skipped until https://github.com/elastic/elasticsearch/pull/77109 is fixed - describe.skip('job on data set with date_nanos time field', function () { + describe('job on data set with date_nanos time field', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/event_rate_nanos'); From 4b33115892fa583782c627e73b7617fd91e45605 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 14 Sep 2021 11:42:08 -0400 Subject: [PATCH 23/39] [Uptime][Synthetics Integration] add browser callout (#108935) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: shahzad31 --- .../fleet_package/custom_fields.test.tsx | 5 ++++ .../fleet_package/custom_fields.tsx | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index 5bcd235b9b60e..3a13be1aa3f68 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -203,6 +203,11 @@ describe('', () => { getAllByLabelText('Zip URL').forEach((node) => { expect(node).toBeInTheDocument(); }); + expect( + getByText( + /To create a "Browser" monitor, please ensure you are using the elastic-agent-complete Docker container, which contains the dependencies to run these mon/ + ) + ).toBeInTheDocument(); // ensure at least one browser advanced option is present advancedOptionsButton = getByText('Advanced Browser options'); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 87f7a98aa4a6f..d641df9c44c5f 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -15,6 +15,8 @@ import { EuiSpacer, EuiDescribedFormGroup, EuiCheckbox, + EuiCallOut, + EuiLink, } from '@elastic/eui'; import { ConfigKeys, DataStream, Validation } from './types'; import { useMonitorTypeContext } from './contexts'; @@ -122,6 +124,34 @@ export const CustomFields = memo( /> )} + + {isBrowser && ( + + + + ), + }} + /> + } + iconType="help" + size="s" + /> + )} + {renderSimpleFields(monitorType)} From eeca80918a1e5d2f14e1f52de7ddca89d37ae403 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 14 Sep 2021 11:58:27 -0400 Subject: [PATCH 24/39] [Security Solution][Endpoint] Unit test for the `ArtifactEntryCard` (#111995) * Test file for ArtifactEntryCard * Fix i18n pluralization * refactor mapper of trusted apps for tags-to-effectScope from server code to common * determine EffectScope for Exceptions based on tags * Fix margin on artifact card divider * Updated snapshot --- .../endpoint/service/trusted_apps/mapping.ts | 30 +++ .../artifact_entry_card.test.tsx | 241 ++++++++++++++++++ .../artifact_entry_card.tsx | 2 +- .../components/translations.ts | 2 +- .../hooks/use_normalized_artifact.ts | 11 +- .../__snapshots__/index.test.tsx.snap | 60 ++--- .../endpoint/routes/trusted_apps/mapping.ts | 24 +- 7 files changed, 317 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts new file mode 100644 index 0000000000000..ddc366f49ed95 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/mapping.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EffectScope } from '../../types'; + +export const POLICY_REFERENCE_PREFIX = 'policy:'; + +/** + * Looks at an array of `tags` (attributed defined on the `ExceptionListItemSchema`) and returns back + * the `EffectScope` of based on the data in the array + * @param tags + */ +export const tagsToEffectScope = (tags: string[]): EffectScope => { + const policyReferenceTags = tags.filter((tag) => tag.startsWith(POLICY_REFERENCE_PREFIX)); + + if (policyReferenceTags.some((tag) => tag === `${POLICY_REFERENCE_PREFIX}all`)) { + return { + type: 'global', + }; + } else { + return { + type: 'policy', + policies: policyReferenceTags.map((tag) => tag.substr(POLICY_REFERENCE_PREFIX.length)), + }; + } +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx new file mode 100644 index 0000000000000..e6e4bb0c2643c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { cloneDeep } from 'lodash'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { ArtifactEntryCard, ArtifactEntryCardProps } from './artifact_entry_card'; +import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator'; +import { act, fireEvent, getByTestId } from '@testing-library/react'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { AnyArtifact } from './types'; +import { isTrustedApp } from './hooks/use_normalized_artifact'; + +const getCommonItemDataOverrides = () => { + return { + name: 'some internal app', + description: 'this app is trusted by the company', + created_at: new Date('2021-07-01').toISOString(), + }; +}; + +const getTrustedAppProvider = () => + new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides()); + +const getExceptionProvider = () => { + // cloneDeep needed because exception mock generator uses state across instances + return cloneDeep( + getExceptionListItemSchemaMock({ + ...getCommonItemDataOverrides(), + os_types: ['windows'], + updated_at: new Date().toISOString(), + created_by: 'Justa', + updated_by: 'Mara', + entries: [ + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: '1234234659af249ddf3e40864e9fb241', + }, + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: '/one/two/three', + }, + ], + tags: ['policy:all'], + }) + ); +}; + +describe.each([ + ['trusted apps', getTrustedAppProvider], + ['exceptions/event filters', getExceptionProvider], +])('when using the ArtifactEntryCard component with %s', (_, generateItem) => { + let item: AnyArtifact; + let appTestContext: AppContextTestRender; + let renderResult: ReturnType; + let render: ( + props?: Partial + ) => ReturnType; + + beforeEach(() => { + item = generateItem(); + appTestContext = createAppRootMockRenderer(); + render = (props = {}) => { + renderResult = appTestContext.render( + + ); + return renderResult; + }; + }); + + it('should display title and who has created and updated it last', async () => { + render(); + + expect(renderResult.getByTestId('testCard-header-title').textContent).toEqual( + 'some internal app' + ); + expect(renderResult.getByTestId('testCard-subHeader-touchedBy-createdBy').textContent).toEqual( + 'Created byJJusta' + ); + expect(renderResult.getByTestId('testCard-subHeader-touchedBy-updatedBy').textContent).toEqual( + 'Updated byMMara' + ); + }); + + it('should display Global effected scope', async () => { + render(); + + expect(renderResult.getByTestId('testCard-subHeader-effectScope-value').textContent).toEqual( + 'Applied globally' + ); + }); + + it('should display dates in expected format', () => { + render(); + + expect(renderResult.getByTestId('testCard-header-updated').textContent).toEqual( + expect.stringMatching(/Last updated(\s seconds? ago|now)/) + ); + }); + + it('should display description if one exists', async () => { + render(); + + expect(renderResult.getByTestId('testCard-description').textContent).toEqual(item.description); + }); + + it('should display default empty value if description does not exist', async () => { + item.description = undefined; + render(); + + expect(renderResult.getByTestId('testCard-description').textContent).toEqual('—'); + }); + + it('should display OS and criteria conditions', () => { + render(); + + expect(renderResult.getByTestId('testCard-criteriaConditions').textContent).toEqual( + ' OSIS WindowsAND process.hash.*IS 1234234659af249ddf3e40864e9fb241AND process.executable.caselessIS /one/two/three' + ); + }); + + it('should NOT show the action menu button if no actions were provided', async () => { + render(); + const menuButton = await renderResult.queryByTestId('testCard-header-actions-button'); + + expect(menuButton).toBeNull(); + }); + + describe('and actions were defined', () => { + let actions: ArtifactEntryCardProps['actions']; + + beforeEach(() => { + actions = [ + { + 'data-test-subj': 'test-action', + children: 'action one', + }, + ]; + }); + + it('should show the actions icon when actions were defined', () => { + render({ actions }); + + expect(renderResult.getByTestId('testCard-header-actions-button')).not.toBeNull(); + }); + + it('should show popup with defined actions', async () => { + render({ actions }); + await act(async () => { + await fireEvent.click(renderResult.getByTestId('testCard-header-actions-button')); + }); + + const bodyHtmlElement = renderResult.baseElement as HTMLElement; + + expect(getByTestId(bodyHtmlElement, 'testCard-header-actions-popoverPanel')).not.toBeNull(); + expect(getByTestId(bodyHtmlElement, 'test-action')).not.toBeNull(); + }); + }); + + describe('and artifact is defined per policy', () => { + let policies: ArtifactEntryCardProps['policies']; + + beforeEach(() => { + if (isTrustedApp(item)) { + item.effectScope = { + type: 'policy', + policies: ['policy-1'], + }; + } else { + item.tags = ['policy:policy-1']; + } + + policies = { + 'policy-1': { + children: 'Policy one', + 'data-test-subj': 'policyMenuItem', + }, + }; + }); + + it('should display correct label with count of policies', () => { + render({ policies }); + + expect(renderResult.getByTestId('testCard-subHeader-effectScope-value').textContent).toEqual( + 'Applied to 1 policy' + ); + }); + + it('should display effected scope as a button', () => { + render({ policies }); + + expect( + renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button') + ).not.toBeNull(); + }); + + it('should show popup menu with list of associated policies when clicked', async () => { + render({ policies }); + await act(async () => { + await fireEvent.click( + renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button') + ); + }); + + expect( + renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel') + ).not.toBeNull(); + + expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual('Policy one'); + }); + + it('should display policy ID if no policy menu item found in `policies` prop', async () => { + render(); + await act(async () => { + await fireEvent.click( + renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button') + ); + }); + + expect( + renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel') + ).not.toBeNull(); + + expect(renderResult.getByText('policy-1').textContent).not.toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx index 14c4e6b947988..4adb81411395a 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx @@ -102,7 +102,7 @@ export const ArtifactEntryCard = memo( - + { return i18n.translate('xpack.securitySolution.artifactCard.policyEffectScope', { - defaultMessage: 'Applied to {count} policies', + defaultMessage: 'Applied to {count} {count, plural, one {policy} other {policies}}', values: { count: policyCount, }, diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts index 175731ee57acb..78d7bd2d2f804 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts @@ -10,7 +10,8 @@ import { useMemo } from 'react'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { AnyArtifact, ArtifactInfo } from '../types'; -import { TrustedApp } from '../../../../../common/endpoint/types'; +import { EffectScope, TrustedApp } from '../../../../../common/endpoint/types'; +import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping'; /** * Takes in any artifact and return back a new data structure used internally with by the card's components @@ -37,12 +38,12 @@ export const useNormalizedArtifact = (item: AnyArtifact): ArtifactInfo => { description, entries: (entries as unknown) as ArtifactInfo['entries'], os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item), - effectScope: isTrustedApp(item) ? item.effectScope : { type: 'global' }, + effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item), }; }, [item]); }; -const isTrustedApp = (item: AnyArtifact): item is TrustedApp => { +export const isTrustedApp = (item: AnyArtifact): item is TrustedApp => { return 'effectScope' in item; }; @@ -50,3 +51,7 @@ const getOsFromExceptionItem = (item: ExceptionListItemSchema): string => { // FYI: Exceptions seem to allow for items to be assigned to more than one OS, unlike Event Filters and Trusted Apps return item.os_types.join(', '); }; + +const getEffectScopeFromExceptionItem = (item: ExceptionListItemSchema): EffectScope => { + return tagsToEffectScope(item.tags); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 3087914a438ef..236a93d63bcee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -744,7 +744,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `





























}; type Mapping = { [K in T]: U }; @@ -48,7 +52,6 @@ const OPERATING_SYSTEM_TO_OS_TYPE: Mapping = { [OperatingSystem.WINDOWS]: 'windows', }; -const POLICY_REFERENCE_PREFIX = 'policy:'; const OPERATOR_VALUE = 'included'; const filterUndefined = (list: Array): T[] => { @@ -63,21 +66,6 @@ export const createConditionEntry = ( return { field, value, type, operator: OPERATOR_VALUE }; }; -export const tagsToEffectScope = (tags: string[]): EffectScope => { - const policyReferenceTags = tags.filter((tag) => tag.startsWith(POLICY_REFERENCE_PREFIX)); - - if (policyReferenceTags.some((tag) => tag === `${POLICY_REFERENCE_PREFIX}all`)) { - return { - type: 'global', - }; - } else { - return { - type: 'policy', - policies: policyReferenceTags.map((tag) => tag.substr(POLICY_REFERENCE_PREFIX.length)), - }; - } -}; - export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => { return entries.reduce((result, entry) => { if (entry.field.startsWith('process.hash') && entry.type === 'match') { From ad5def63bff4ba96e196b525b789a768968b53d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 14 Sep 2021 12:35:31 -0400 Subject: [PATCH 25/39] [APM] By passing no data screen on settings pages (#111986) * by passing no data screen on settings pages * addressing PR comments * adjusting teste --- .../integration/power_user/no_data_screen.ts | 65 +++++++++++++++++++ .../routing/templates/apm_main_template.tsx | 12 +++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts new file mode 100644 index 0000000000000..2d5c2a6f16228 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* eslint-disable @typescript-eslint/naming-convention */ + +const apmIndicesSaveURL = '/api/apm/settings/apm-indices/save'; + +describe('No data screen', () => { + describe('bypass no data screen on settings pages', () => { + beforeEach(() => { + cy.loginAsPowerUser(); + }); + + before(() => { + // Change default indices + cy.request({ + url: apmIndicesSaveURL, + method: 'POST', + body: { + 'apm_oss.sourcemapIndices': 'foo-*', + 'apm_oss.errorIndices': 'foo-*', + 'apm_oss.onboardingIndices': 'foo-*', + 'apm_oss.spanIndices': 'foo-*', + 'apm_oss.transactionIndices': 'foo-*', + 'apm_oss.metricsIndices': 'foo-*', + }, + headers: { + 'kbn-xsrf': true, + }, + auth: { user: 'apm_power_user', pass: 'changeme' }, + }); + }); + + it('shows no data screen instead of service inventory', () => { + cy.visit('/app/apm/'); + cy.contains('Welcome to Elastic Observability!'); + }); + it('shows settings page', () => { + cy.visit('/app/apm/settings'); + cy.contains('Welcome to Elastic Observability!').should('not.exist'); + cy.get('h1').contains('Settings'); + }); + + after(() => { + // reset to default indices + cy.request({ + url: apmIndicesSaveURL, + method: 'POST', + body: { + 'apm_oss.sourcemapIndices': '', + 'apm_oss.errorIndices': '', + 'apm_oss.onboardingIndices': '', + 'apm_oss.spanIndices': '', + 'apm_oss.transactionIndices': '', + 'apm_oss.metricsIndices': '', + }, + headers: { 'kbn-xsrf': true }, + auth: { user: 'apm_power_user', pass: 'changeme' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index f4494f1841ba3..6a4ab5d7d9bc5 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -7,12 +7,16 @@ import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; import React from 'react'; +import { useLocation } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useFetcher } from '../../../hooks/use_fetcher'; import { ApmPluginStartDeps } from '../../../plugin'; import { ApmEnvironmentFilter } from '../../shared/EnvironmentFilter'; import { getNoDataConfig } from './no_data_config'; +// Paths that must skip the no data screen +const bypassNoDataScreenPaths = ['/settings']; + /* * This template contains: * - The Shared Observability Nav (https://github.com/elastic/kibana/blob/f7698bd8aa8787d683c728300ba4ca52b202369c/x-pack/plugins/observability/public/components/shared/page_template/README.md) @@ -32,6 +36,8 @@ export function ApmMainTemplate({ pageHeader?: EuiPageHeaderProps; children: React.ReactNode; } & EuiPageTemplateProps) { + const location = useLocation(); + const { services } = useKibana(); const { http, docLinks } = services; const basePath = http?.basePath.get(); @@ -49,9 +55,13 @@ export function ApmMainTemplate({ hasData: data?.hasData, }); + const shouldBypassNoDataScreen = bypassNoDataScreenPaths.some((path) => + location.pathname.includes(path) + ); + return ( ], From 458a57939b9037258f75e12e1b8791d06974e380 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 14 Sep 2021 17:40:00 +0100 Subject: [PATCH 26/39] [QA][SO INFO SVC] Docs for using with jq (#112129) --- .../services/saved_object_info/README.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 test/common/services/saved_object_info/README.md diff --git a/test/common/services/saved_object_info/README.md b/test/common/services/saved_object_info/README.md new file mode 100644 index 0000000000000..5f081e48e2639 --- /dev/null +++ b/test/common/services/saved_object_info/README.md @@ -0,0 +1,88 @@ +# Tips for using the SO INFO SVC CLI with JQ + +## Myriad ways to use jq to discern discrete info from the svc +Below, I will leave out the so types call, which is: +`node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes --json` + +#### At time of this writing, without `jq`, the svc call result was: +`node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes` + +``` +### Saved Object Types Count: 5 +[ + { + doc_count: 5, + key: 'canvas-workpad-template' + }, + { + doc_count: 1, + key: 'apm-telemetry' + }, + { + doc_count: 1, + key: 'config' + }, + { + doc_count: 1, + key: 'event_loop_delays_daily' + }, + { + doc_count: 1, + key: 'space' + } +] +``` + +### Show the keys only +`jq '.[] | (.key)'` || `jq '.[] | .key'` + +``` +"canvas-workpad-template" +"apm-telemetry" +"config" +"event_loop_delays_daily" +"space" +``` + + +### Show the count of a specific Saved Object type +Eg. Count of spaces +`jq '.[] | select(.key =="space")'` + +``` +{ + "key": "space", + "doc_count": 1 +} +``` + +### Show the saved objects with a count greater than 2 +`jq '.[] | select(.doc_count > 2)'` + +``` +{ + "key": "canvas-workpad-template", + "doc_count": 5 +} +``` + +### Show the TOTAL count of ALL Saved Object types +`jq 'reduce .[].doc_count as $item (0; . + $item)'` + +``` +9 +``` + +#### Diffing +You could add a log file to your git index +and then write to the file again and simply use +`git diff` to see the difference. + +Similarly, you could write out two different files +and use your OS's default diff-ing program. +On OSX, I use `diff before.txt after.txt` + +Lastly, you could have two separate terminal +windows and use your eyes to spot differences, +that is if you expect differences. + From 2262cac584f1c9549a19ca7bf0998c7426ebfd27 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 14 Sep 2021 18:53:17 +0200 Subject: [PATCH 27/39] [Lens] Fix add to dashboard functional test flakiness (#111887) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../functional/apps/lens/add_to_dashboard.ts | 23 ++----------------- .../functional/apps/lens/drag_and_drop.ts | 3 +-- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/add_to_dashboard.ts index 55d8ff9cf7621..01338b6ee03d3 100644 --- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/add_to_dashboard.ts @@ -62,8 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }; - // FLAKY: https://github.com/elastic/kibana/issues/111628 - describe.skip('lens add-to-dashboards tests', () => { + describe('lens add-to-dashboards tests', () => { it('should allow new lens to be added by value to a new dashboard', async () => { await createNewLens(); await PageObjects.lens.save('New Lens from Modal', false, false, false, 'new'); @@ -238,17 +237,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // issue #111104 it('should add a Lens heatmap to the dashboard', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - - await PageObjects.dashboard.saveDashboard('My Wonderful Heatmap dashboard'); - await PageObjects.dashboard.gotoDashboardLandingPage(); - await listingTable.searchAndExpectItemsCount( - 'dashboard', - 'My Wonderful Heatmap dashboard', - 1 - ); - await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -275,14 +263,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.lens.save( - 'New Lens Heatmap', - false, - false, - true, - 'existing', - 'My Wonderful Heatmap dashboard' - ); + await PageObjects.lens.save('New Lens Heatmap', false, false, true, 'new'); await PageObjects.dashboard.waitForRenderComplete(); diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index b53d4370d561e..e7b7ba18d62fb 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -11,8 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); - // FLAKY: https://github.com/elastic/kibana/issues/108352 - describe.skip('lens drag and drop tests', () => { + describe('lens drag and drop tests', () => { describe('basic drag and drop', () => { it('should construct the basic split xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); From 9b9286fc578505e62a9d93cdf2865d65e674fbcb Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Tue, 14 Sep 2021 19:20:10 +0200 Subject: [PATCH 28/39] Remove executionLog to reduce memory consumption (#111911) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../logging/appenders/file/file_appender.ts | 6 +- ...grations_state_action_machine.test.ts.snap | 937 ++++++++++++++++++ .../integration_tests/cleanup.test.ts | 1 + .../migrations_state_action_machine.test.ts | 326 +----- .../migrations_state_action_machine.ts | 95 +- .../migrations_state_machine_cleanup.ts | 17 +- 6 files changed, 999 insertions(+), 383 deletions(-) create mode 100644 src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap diff --git a/src/core/server/logging/appenders/file/file_appender.ts b/src/core/server/logging/appenders/file/file_appender.ts index be46c261dc996..3c22a37038bcc 100644 --- a/src/core/server/logging/appenders/file/file_appender.ts +++ b/src/core/server/logging/appenders/file/file_appender.ts @@ -65,8 +65,10 @@ export class FileAppender implements DisposableAppender { return resolve(); } - this.outputStream.end(() => { - this.outputStream = undefined; + const outputStream = this.outputStream; + this.outputStream = undefined; + + outputStream.end(() => { resolve(); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap new file mode 100644 index 0000000000000..c3512d8fd50bd --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -0,0 +1,937 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`migrationsStateActionMachine logs state transitions, messages in state.logs and action responses when reaching DONE 1`] = ` +Object { + "debug": Array [ + Array [ + "[.my-so-index] INIT RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] INIT -> LEGACY_REINDEX. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "LEGACY_REINDEX", + "currentAlias": ".my-so-index", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_REINDEX control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "outdatedDocuments": Array [], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [], + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, + }, + Object { + "match": Object { + "search-session.persisted": false, + }, + }, + ], + }, + }, + ], + }, + }, + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_REINDEX RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "LEGACY_DELETE", + "currentAlias": ".my-so-index", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_REINDEX control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "outdatedDocuments": Array [], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [], + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, + }, + Object { + "match": Object { + "search-session.persisted": false, + }, + }, + ], + }, + }, + ], + }, + }, + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> LEGACY_DELETE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "LEGACY_DELETE", + "currentAlias": ".my-so-index", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_REINDEX control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "outdatedDocuments": Array [], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [], + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, + }, + Object { + "match": Object { + "search-session.persisted": false, + }, + }, + ], + }, + }, + ], + }, + }, + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> DONE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "DONE", + "currentAlias": ".my-so-index", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_REINDEX control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from DONE control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "outdatedDocuments": Array [], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [], + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, + }, + Object { + "match": Object { + "search-session.persisted": false, + }, + }, + ], + }, + }, + ], + }, + }, + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + ], + "error": Array [], + "fatal": Array [], + "info": Array [ + Array [ + "[.my-so-index] Log from LEGACY_REINDEX control state", + ], + Array [ + "[.my-so-index] INIT -> LEGACY_REINDEX. took: 0ms.", + ], + Array [ + "[.my-so-index] Log from LEGACY_DELETE control state", + ], + Array [ + "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE. took: 0ms.", + ], + Array [ + "[.my-so-index] Log from LEGACY_DELETE control state", + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> LEGACY_DELETE. took: 0ms.", + ], + Array [ + "[.my-so-index] Log from DONE control state", + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> DONE. took: 0ms.", + ], + ], + "log": Array [], + "trace": Array [], + "warn": Array [], +} +`; + +exports[`migrationsStateActionMachine logs state transitions, messages in state.logs and action responses when reaching FATAL 1`] = ` +Object { + "debug": Array [ + Array [ + "[.my-so-index] INIT RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] INIT -> LEGACY_DELETE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "LEGACY_DELETE", + "currentAlias": ".my-so-index", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "outdatedDocuments": Array [ + Object { + "_id": "1234", + }, + ], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "reason": "the fatal reason", + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [ + Array [ + Object { + "_id": "1234", + }, + ], + ], + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, + }, + Object { + "match": Object { + "search-session.persisted": false, + }, + }, + ], + }, + }, + ], + }, + }, + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> FATAL. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "FATAL", + "currentAlias": ".my-so-index", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from FATAL control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "outdatedDocuments": Array [ + Object { + "_id": "1234", + }, + ], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "reason": "the fatal reason", + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [ + Array [ + Object { + "_id": "1234", + }, + ], + ], + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, + }, + Object { + "match": Object { + "search-session.persisted": false, + }, + }, + ], + }, + }, + ], + }, + }, + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + ], + "error": Array [], + "fatal": Array [], + "info": Array [ + Array [ + "[.my-so-index] Log from LEGACY_DELETE control state", + ], + Array [ + "[.my-so-index] INIT -> LEGACY_DELETE. took: 0ms.", + ], + Array [ + "[.my-so-index] Log from FATAL control state", + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> FATAL. took: 0ms.", + ], + ], + "log": Array [], + "trace": Array [], + "warn": Array [], +} +`; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts index bb408d14df6d7..d76bbc786cffc 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -44,6 +44,7 @@ function createRoot() { { name: 'root', appenders: ['file'], + level: 'debug', // DEBUG logs are required to retrieve the PIT _id from the action response logs }, ], }, diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index a312ac6be0c3d..21468d7552320 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -77,7 +77,7 @@ describe('migrationsStateActionMachine', () => { }; }; - it('logs state transitions, messages in state.logs and action responses', async () => { + it('logs state transitions, messages in state.logs and action responses when reaching DONE', async () => { await migrationStateActionMachine({ initialState, logger: mockLogger.get(), @@ -88,71 +88,23 @@ describe('migrationsStateActionMachine', () => { const logs = loggingSystemMock.collect(mockLogger); const doneLog = logs.info.splice(8, 1)[0][0]; expect(doneLog).toMatch(/\[.my-so-index\] Migration completed after \d+ms/); - expect(logs).toMatchInlineSnapshot(` - Object { - "debug": Array [ - Array [ - "[.my-so-index] INIT RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_REINDEX RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - ], - "error": Array [], - "fatal": Array [], - "info": Array [ - Array [ - "[.my-so-index] Log from LEGACY_REINDEX control state", - ], - Array [ - "[.my-so-index] INIT -> LEGACY_REINDEX. took: 0ms.", - ], - Array [ - "[.my-so-index] Log from LEGACY_DELETE control state", - ], - Array [ - "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE. took: 0ms.", - ], - Array [ - "[.my-so-index] Log from LEGACY_DELETE control state", - ], - Array [ - "[.my-so-index] LEGACY_DELETE -> LEGACY_DELETE. took: 0ms.", - ], - Array [ - "[.my-so-index] Log from DONE control state", - ], - Array [ - "[.my-so-index] LEGACY_DELETE -> DONE. took: 0ms.", - ], - ], - "log": Array [], - "trace": Array [], - "warn": Array [], - } - `); + expect(logs).toMatchSnapshot(); + }); + + it('logs state transitions, messages in state.logs and action responses when reaching FATAL', async () => { + await migrationStateActionMachine({ + initialState: { + ...initialState, + reason: 'the fatal reason', + outdatedDocuments: [{ _id: '1234', password: 'sensitive password' }], + transformedDocBatches: [[{ _id: '1234', password: 'sensitive transformed password' }]], + } as State, + logger: mockLogger.get(), + model: transitionModel(['LEGACY_DELETE', 'FATAL']), + next, + client: esClient, + }).catch((err) => err); + expect(loggingSystemMock.collect(mockLogger)).toMatchSnapshot(); }); // see https://github.com/elastic/kibana/issues/98406 @@ -196,6 +148,7 @@ describe('migrationsStateActionMachine', () => { }) ).resolves.toEqual(expect.anything()); }); + it('resolves with migrated status if some sourceIndex in the DONE state', async () => { await expect( migrationStateActionMachine({ @@ -207,6 +160,7 @@ describe('migrationsStateActionMachine', () => { }) ).resolves.toEqual(expect.objectContaining({ status: 'migrated' })); }); + it('resolves with patched status if none sourceIndex in the DONE state', async () => { await expect( migrationStateActionMachine({ @@ -218,6 +172,7 @@ describe('migrationsStateActionMachine', () => { }) ).resolves.toEqual(expect.objectContaining({ status: 'patched' })); }); + it('rejects with error message when reaching the FATAL state', async () => { await expect( migrationStateActionMachine({ @@ -231,127 +186,8 @@ describe('migrationsStateActionMachine', () => { `[Error: Unable to complete saved object migrations for the [.my-so-index] index: the fatal reason]` ); }); - it('logs all state transitions and action responses when reaching the FATAL state', async () => { - await migrationStateActionMachine({ - initialState: { - ...initialState, - reason: 'the fatal reason', - outdatedDocuments: [{ _id: '1234', password: 'sensitive password' }], - transformedDocBatches: [[{ _id: '1234', password: 'sensitive transformed password' }]], - } as State, - logger: mockLogger.get(), - model: transitionModel(['LEGACY_DELETE', 'FATAL']), - next, - client: esClient, - }).catch((err) => err); - // Ignore the first 4 log entries that come from our model - const executionLogLogs = loggingSystemMock.collect(mockLogger).info.slice(4); - expect(executionLogLogs).toEqual([ - [ - '[.my-so-index] INIT RESPONSE', - { - _tag: 'Right', - right: 'response', - }, - ], - [ - '[.my-so-index] INIT -> LEGACY_DELETE', - { - kibana: { - migrationState: { - batchSize: 1000, - maxBatchSizeBytes: 1e8, - controlState: 'LEGACY_DELETE', - currentAlias: '.my-so-index', - excludeFromUpgradeFilterHooks: {}, - indexPrefix: '.my-so-index', - kibanaVersion: '7.11.0', - knownTypes: [], - legacyIndex: '.my-so-index', - logs: [ - { - level: 'info', - message: 'Log from LEGACY_DELETE control state', - }, - ], - outdatedDocuments: [{ _id: '1234' }], - outdatedDocumentsQuery: expect.any(Object), - preMigrationScript: { - _tag: 'None', - }, - reason: 'the fatal reason', - retryAttempts: 5, - retryCount: 0, - retryDelay: 0, - targetIndexMappings: { - properties: {}, - }, - tempIndex: '.my-so-index_7.11.0_reindex_temp', - tempIndexMappings: expect.any(Object), - transformedDocBatches: [[{ _id: '1234' }]], - unusedTypesQuery: expect.any(Object), - versionAlias: '.my-so-index_7.11.0', - versionIndex: '.my-so-index_7.11.0_001', - }, - }, - }, - ], - [ - '[.my-so-index] LEGACY_DELETE RESPONSE', - { - _tag: 'Right', - right: 'response', - }, - ], - [ - '[.my-so-index] LEGACY_DELETE -> FATAL', - { - kibana: { - migrationState: { - batchSize: 1000, - maxBatchSizeBytes: 1e8, - controlState: 'FATAL', - currentAlias: '.my-so-index', - excludeFromUpgradeFilterHooks: {}, - indexPrefix: '.my-so-index', - kibanaVersion: '7.11.0', - knownTypes: [], - legacyIndex: '.my-so-index', - logs: [ - { - level: 'info', - message: 'Log from LEGACY_DELETE control state', - }, - { - level: 'info', - message: 'Log from FATAL control state', - }, - ], - outdatedDocuments: [{ _id: '1234' }], - outdatedDocumentsQuery: expect.any(Object), - preMigrationScript: { - _tag: 'None', - }, - reason: 'the fatal reason', - retryAttempts: 5, - retryCount: 0, - retryDelay: 0, - targetIndexMappings: { - properties: {}, - }, - tempIndex: '.my-so-index_7.11.0_reindex_temp', - tempIndexMappings: expect.any(Object), - transformedDocBatches: [[{ _id: '1234' }]], - unusedTypesQuery: expect.any(Object), - versionAlias: '.my-so-index_7.11.0', - versionIndex: '.my-so-index_7.11.0_001', - }, - }, - }, - ], - ]); - }); - it('rejects and logs the error when an action throws with an ResponseError', async () => { + + it('rejects and logs the error when an action throws with a ResponseError', async () => { await expect( migrationStateActionMachine({ initialState: { ...initialState, reason: 'the fatal reason' } as State, @@ -384,9 +220,6 @@ describe('migrationsStateActionMachine', () => { Array [ "[.my-so-index] Unexpected Elasticsearch ResponseError: statusCode: 200, method: POST, url: /mock error: [snapshot_in_progress_exception]: Cannot delete indices that are being snapshotted,", ], - Array [ - "[.my-so-index] migration failed, dumping execution log:", - ], ], "fatal": Array [], "info": Array [], @@ -417,9 +250,6 @@ describe('migrationsStateActionMachine', () => { Array [ [Error: this action throws], ], - Array [ - "[.my-so-index] migration failed, dumping execution log:", - ], ], "fatal": Array [], "info": Array [], @@ -429,116 +259,6 @@ describe('migrationsStateActionMachine', () => { } `); }); - it('logs all state transitions and action responses when an action throws', async () => { - try { - await migrationStateActionMachine({ - initialState: { ...initialState, reason: 'the fatal reason' } as State, - logger: mockLogger.get(), - model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']), - next: (state) => { - if (state.controlState === 'LEGACY_DELETE') throw new Error('this action throws'); - return () => Promise.resolve('hello'); - }, - client: esClient, - }); - } catch (e) { - /** ignore */ - } - // Ignore the first 4 log entries that come from our model - const executionLogLogs = loggingSystemMock.collect(mockLogger).info.slice(4); - expect(executionLogLogs).toEqual([ - ['[.my-so-index] INIT RESPONSE', 'hello'], - [ - '[.my-so-index] INIT -> LEGACY_REINDEX', - { - kibana: { - migrationState: { - batchSize: 1000, - maxBatchSizeBytes: 1e8, - controlState: 'LEGACY_REINDEX', - currentAlias: '.my-so-index', - excludeFromUpgradeFilterHooks: {}, - indexPrefix: '.my-so-index', - kibanaVersion: '7.11.0', - knownTypes: [], - legacyIndex: '.my-so-index', - logs: [ - { - level: 'info', - message: 'Log from LEGACY_REINDEX control state', - }, - ], - outdatedDocuments: [], - outdatedDocumentsQuery: expect.any(Object), - preMigrationScript: { - _tag: 'None', - }, - reason: 'the fatal reason', - retryAttempts: 5, - retryCount: 0, - retryDelay: 0, - targetIndexMappings: { - properties: {}, - }, - tempIndex: '.my-so-index_7.11.0_reindex_temp', - tempIndexMappings: expect.any(Object), - transformedDocBatches: [], - unusedTypesQuery: expect.any(Object), - versionAlias: '.my-so-index_7.11.0', - versionIndex: '.my-so-index_7.11.0_001', - }, - }, - }, - ], - ['[.my-so-index] LEGACY_REINDEX RESPONSE', 'hello'], - [ - '[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE', - { - kibana: { - migrationState: { - batchSize: 1000, - maxBatchSizeBytes: 1e8, - controlState: 'LEGACY_DELETE', - currentAlias: '.my-so-index', - excludeFromUpgradeFilterHooks: {}, - indexPrefix: '.my-so-index', - kibanaVersion: '7.11.0', - knownTypes: [], - legacyIndex: '.my-so-index', - logs: [ - { - level: 'info', - message: 'Log from LEGACY_REINDEX control state', - }, - { - level: 'info', - message: 'Log from LEGACY_DELETE control state', - }, - ], - outdatedDocuments: [], - outdatedDocumentsQuery: expect.any(Object), - preMigrationScript: { - _tag: 'None', - }, - reason: 'the fatal reason', - retryAttempts: 5, - retryCount: 0, - retryDelay: 0, - targetIndexMappings: { - properties: {}, - }, - tempIndex: '.my-so-index_7.11.0_reindex_temp', - tempIndexMappings: expect.any(Object), - transformedDocBatches: [], - unusedTypesQuery: expect.any(Object), - versionAlias: '.my-so-index_7.11.0', - versionIndex: '.my-so-index_7.11.0_001', - }, - }, - }, - ], - ]); - }); describe('cleanup', () => { beforeEach(() => { cleanupMock.mockClear(); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index 58c299b77fc60..d4ad724911277 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -16,41 +16,24 @@ import { cleanup } from './migrations_state_machine_cleanup'; import { ReindexSourceToTempIndex, ReindexSourceToTempIndexBulk, State } from './types'; import { SavedObjectsRawDoc } from '../serialization'; -interface StateLogMeta extends LogMeta { +interface StateTransitionLogMeta extends LogMeta { kibana: { - migrationState: State; + migrations: { + state: State; + duration: number; + }; }; } -/** @internal */ -export type ExecutionLog = Array< - | { - type: 'transition'; - prevControlState: State['controlState']; - controlState: State['controlState']; - state: State; - } - | { - type: 'response'; - controlState: State['controlState']; - res: unknown; - } - | { - type: 'cleanup'; - state: State; - message: string; - } ->; - const logStateTransition = ( logger: Logger, logMessagePrefix: string, - oldState: State, - newState: State, + prevState: State, + currState: State, tookMs: number ) => { - if (newState.logs.length > oldState.logs.length) { - newState.logs.slice(oldState.logs.length).forEach(({ message, level }) => { + if (currState.logs.length > prevState.logs.length) { + currState.logs.slice(prevState.logs.length).forEach(({ message, level }) => { switch (level) { case 'error': return logger.error(logMessagePrefix + message); @@ -65,7 +48,18 @@ const logStateTransition = ( } logger.info( - logMessagePrefix + `${oldState.controlState} -> ${newState.controlState}. took: ${tookMs}ms.` + logMessagePrefix + `${prevState.controlState} -> ${currState.controlState}. took: ${tookMs}ms.` + ); + logger.debug( + logMessagePrefix + `${prevState.controlState} -> ${currState.controlState}. took: ${tookMs}ms.`, + { + kibana: { + migrations: { + state: currState, + duration: tookMs, + }, + }, + } ); }; @@ -77,24 +71,6 @@ const logActionResponse = ( ) => { logger.debug(logMessagePrefix + `${state.controlState} RESPONSE`, res as LogMeta); }; -const dumpExecutionLog = (logger: Logger, logMessagePrefix: string, executionLog: ExecutionLog) => { - logger.error(logMessagePrefix + 'migration failed, dumping execution log:'); - executionLog.forEach((log) => { - if (log.type === 'transition') { - logger.info( - logMessagePrefix + `${log.prevControlState} -> ${log.controlState}`, - { - kibana: { - migrationState: log.state, - }, - } - ); - } - if (log.type === 'response') { - logger.info(logMessagePrefix + `${log.controlState} RESPONSE`, log.res as LogMeta); - } - }); -}; /** * A specialized migrations-specific state-action machine that: @@ -118,7 +94,6 @@ export async function migrationStateActionMachine({ model: Model; client: ElasticsearchClient; }) { - const executionLog: ExecutionLog = []; const startTime = Date.now(); // Since saved object index names usually start with a `.` and can be // configured by users to include several `.`'s we can't use a logger tag to @@ -132,11 +107,6 @@ export async function migrationStateActionMachine({ (state) => next(state), (state, res) => { lastState = state; - executionLog.push({ - type: 'response', - res, - controlState: state.controlState, - }); logActionResponse(logger, logMessagePrefix, state, res); const newState = model(state, res); // Redact the state to reduce the memory consumption and so that we @@ -158,12 +128,7 @@ export async function migrationStateActionMachine({ ).map((batches) => batches.map((doc) => ({ _id: doc._id }))) as [SavedObjectsRawDoc[]], }, }; - executionLog.push({ - type: 'transition', - state: redactedNewState, - controlState: newState.controlState, - prevControlState: state.controlState, - }); + const now = Date.now(); logStateTransition( logger, @@ -195,8 +160,11 @@ export async function migrationStateActionMachine({ }; } } else if (finalState.controlState === 'FATAL') { - await cleanup(client, executionLog, finalState); - dumpExecutionLog(logger, logMessagePrefix, executionLog); + try { + await cleanup(client, finalState); + } catch (e) { + logger.warn('Failed to cleanup after migrations:', e.message); + } return Promise.reject( new Error( `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index: ` + @@ -207,7 +175,11 @@ export async function migrationStateActionMachine({ throw new Error('Invalid terminating control state'); } } catch (e) { - await cleanup(client, executionLog, lastState); + try { + await cleanup(client, lastState); + } catch (err) { + logger.warn('Failed to cleanup after migrations:', err.message); + } if (e instanceof EsErrors.ResponseError) { // Log the failed request. This is very similar to the // elasticsearch-service's debug logs, but we log everything in single @@ -219,15 +191,12 @@ export async function migrationStateActionMachine({ req.statusCode }, method: ${req.method}, url: ${req.url} error: ${getErrorMessage(e)},`; logger.error(logMessagePrefix + failedRequestMessage); - dumpExecutionLog(logger, logMessagePrefix, executionLog); throw new Error( `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index. Please check the health of your Elasticsearch cluster and try again. ${failedRequestMessage}` ); } else { logger.error(e); - dumpExecutionLog(logger, logMessagePrefix, executionLog); - const newError = new Error( `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index. ${e}` ); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts index e9cb33c0aa54a..9c0ef0d1a2cb6 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts @@ -9,23 +9,10 @@ import type { ElasticsearchClient } from '../../elasticsearch'; import * as Actions from './actions'; import type { State } from './types'; -import type { ExecutionLog } from './migrations_state_action_machine'; -export async function cleanup( - client: ElasticsearchClient, - executionLog: ExecutionLog, - state?: State -) { +export async function cleanup(client: ElasticsearchClient, state?: State) { if (!state) return; if ('sourceIndexPitId' in state) { - try { - await Actions.closePit({ client, pitId: state.sourceIndexPitId })(); - } catch (e) { - executionLog.push({ - type: 'cleanup', - state, - message: e.message, - }); - } + await Actions.closePit({ client, pitId: state.sourceIndexPitId })(); } } From 65eb7b930af33d8bb8776b157bf6328ee5779210 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 14 Sep 2021 13:24:21 -0400 Subject: [PATCH 29/39] [APM] Fixes no data in service overview transaction breakdown chart (#112123) (#112124) --- .../apm/public/components/app/service_overview/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 5dda443283921..c73d412fb4506 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -163,7 +163,7 @@ export function ServiceOverview() { {!isRumAgent && ( From 2567c4f5fba93c34276d63a9ec7bb750fb21a185 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 14 Sep 2021 13:42:50 -0400 Subject: [PATCH 30/39] [Cases] Add import/export functionality (#110148) * Working export but not import * Adding user action import flag * Adding a few integration tests * Adding connector test * Finishing integration tests * Removing connector after each test to restore original state * Bumping migration version for comment so Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/server/plugin.ts | 4 +- .../cases/server/saved_object_types/cases.ts | 27 ++- .../server/saved_object_types/comments.ts | 5 +- .../import_export/export.ts | 114 ++++++++++ .../cases/server/saved_object_types/index.ts | 2 +- .../server/saved_object_types/user_actions.ts | 3 + ...ingle_case_user_actions_one_comment.ndjson | 5 + ..._case_with_connector_update_to_none.ndjson | 6 + .../tests/common/cases/import_export.ts | 209 ++++++++++++++++++ .../security_and_spaces/tests/common/index.ts | 1 + 10 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/cases/server/saved_object_types/import_export/export.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson create mode 100644 x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index bb1be163585a8..c04e495889a74 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -21,7 +21,7 @@ import { createCaseCommentSavedObjectType, caseConfigureSavedObjectType, caseConnectorMappingsSavedObjectType, - caseSavedObjectType, + createCaseSavedObjectType, caseUserActionSavedObjectType, subCaseSavedObjectType, } from './saved_object_types'; @@ -94,7 +94,7 @@ export class CasePlugin { ); core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); - core.savedObjects.registerType(caseSavedObjectType); + core.savedObjects.registerType(createCaseSavedObjectType(core, this.log)); core.savedObjects.registerType(caseUserActionSavedObjectType); this.log.debug( diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index 199017c36fa3e..a362d77c06626 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -5,11 +5,22 @@ * 2.0. */ -import { SavedObjectsType } from 'src/core/server'; +import { + CoreSetup, + Logger, + SavedObject, + SavedObjectsExportTransformContext, + SavedObjectsType, +} from 'src/core/server'; import { CASE_SAVED_OBJECT } from '../../common'; +import { ESCaseAttributes } from '../services/cases/types'; +import { handleExport } from './import_export/export'; import { caseMigrations } from './migrations'; -export const caseSavedObjectType: SavedObjectsType = { +export const createCaseSavedObjectType = ( + coreSetup: CoreSetup, + logger: Logger +): SavedObjectsType => ({ name: CASE_SAVED_OBJECT, hidden: true, namespaceType: 'single', @@ -144,4 +155,14 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, migrations: caseMigrations, -}; + management: { + importableAndExportable: true, + defaultSearchField: 'title', + icon: 'folderExclamation', + getTitle: (savedObject: SavedObject) => savedObject.attributes.title, + onExport: async ( + context: SavedObjectsExportTransformContext, + objects: Array> + ) => handleExport({ context, objects, coreSetup, logger }), + }, +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index 00985df8ab834..af14123eca580 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -109,5 +109,8 @@ export const createCaseCommentSavedObjectType = ({ }, }, }, - migrations: () => createCommentsMigrations(migrationDeps), + migrations: createCommentsMigrations(migrationDeps), + management: { + importableAndExportable: true, + }, }); diff --git a/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts b/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts new file mode 100644 index 0000000000000..d089079314443 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CoreSetup, + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsExportTransformContext, +} from 'kibana/server'; +import { + CaseUserActionAttributes, + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, + CommentAttributes, + MAX_DOCS_PER_PAGE, + SAVED_OBJECT_TYPES, +} from '../../../common'; +import { createCaseError, defaultSortField } from '../../common'; +import { ESCaseAttributes } from '../../services/cases/types'; + +export async function handleExport({ + context, + objects, + coreSetup, + logger, +}: { + context: SavedObjectsExportTransformContext; + objects: Array>; + coreSetup: CoreSetup; + logger: Logger; +}): Promise>> { + try { + if (objects.length <= 0) { + return []; + } + + const [{ savedObjects }] = await coreSetup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(context.request, { + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); + + const caseIds = objects.map((caseObject) => caseObject.id); + const attachmentsAndUserActionsForCases = await getAttachmentsAndUserActionsForCases( + savedObjectsClient, + caseIds + ); + + return [...objects, ...attachmentsAndUserActionsForCases.flat()]; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve associated objects for exporting of cases: ${error}`, + error, + logger, + }); + } +} + +async function getAttachmentsAndUserActionsForCases( + savedObjectsClient: SavedObjectsClientContract, + caseIds: string[] +): Promise>> { + const [attachments, userActions] = await Promise.all([ + getAssociatedObjects({ + savedObjectsClient, + caseIds, + sortField: defaultSortField, + type: CASE_COMMENT_SAVED_OBJECT, + }), + getAssociatedObjects({ + savedObjectsClient, + caseIds, + sortField: 'action_at', + type: CASE_USER_ACTION_SAVED_OBJECT, + }), + ]); + + return [...attachments, ...userActions]; +} + +async function getAssociatedObjects({ + savedObjectsClient, + caseIds, + sortField, + type, +}: { + savedObjectsClient: SavedObjectsClientContract; + caseIds: string[]; + sortField: string; + type: string; +}): Promise>> { + const references = caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id })); + + const finder = savedObjectsClient.createPointInTimeFinder({ + type, + hasReferenceOperator: 'OR', + hasReference: references, + perPage: MAX_DOCS_PER_PAGE, + sortField, + sortOrder: 'asc', + }); + + let result: Array> = []; + for await (const findResults of finder.find()) { + result = result.concat(findResults.saved_objects); + } + + return result; +} diff --git a/x-pack/plugins/cases/server/saved_object_types/index.ts b/x-pack/plugins/cases/server/saved_object_types/index.ts index 2c39a10f61da7..f6b87d1d480c1 100644 --- a/x-pack/plugins/cases/server/saved_object_types/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { caseSavedObjectType } from './cases'; +export { createCaseSavedObjectType } from './cases'; export { subCaseSavedObjectType } from './sub_case'; export { caseConfigureSavedObjectType } from './configure'; export { createCaseCommentSavedObjectType } from './comments'; diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index 16bb7ac09a6ef..883105982bcb3 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -49,4 +49,7 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { }, }, migrations: userActionsMigrations, + management: { + importableAndExportable: true, + }, }; diff --git a/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson new file mode 100644 index 0000000000000..2fb02f297c6ea --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson @@ -0,0 +1,5 @@ +{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T19:48:01.292Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"a description","external_service":null,"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["some tags"],"title":"A case to export","type":"individual","updated_at":"2021-08-26T19:48:30.151Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases":"7.15.0"},"references":[],"type":"cases","updated_at":"2021-08-26T19:48:30.162Z","version":"WzM0NDEsMV0="} +{"attributes":{"action":"create","action_at":"2021-08-26T19:48:01.292Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case to export\",\"tags\":[\"some tags\"],\"description\":\"a description\",\"connector\":{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"8cb85070-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007281292,7288],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:13.687Z","version":"WzIzODIsMV0="} +{"attributes":{"associationType":"case","comment":"A comment for my case","created_at":"2021-08-26T19:48:30.151Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"owner":"securitySolution","pushed_at":null,"pushed_by":null,"type":"user","updated_at":null,"updated_by":null},"coreMigrationVersion":"8.0.0","id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-comments":"7.16.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007310151,9470],"type":"cases-comments","updated_at":"2021-08-26T19:48:30.161Z","version":"WzM0NDIsMV0="} +{"attributes":{"action":"create","action_at":"2021-08-26T19:48:30.151Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["comment"],"new_value":"{\"comment\":\"A comment for my case\",\"type\":\"user\",\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"9710c840-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"},{"id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1630007310151,9542],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:31.044Z","version":"WzM1MTIsMV0="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":4,"missingRefCount":0,"missingReferences":[]} diff --git a/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson new file mode 100644 index 0000000000000..4f4476df376cc --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson @@ -0,0 +1,6 @@ +{"attributes":{"actionTypeId":".jira","config":{"apiUrl":"https://cases-testing.atlassian.net","projectKey":"TPN"},"isMissingSecrets":true,"name":"A jira connector"},"coreMigrationVersion":"8.0.0","id":"1cd34740-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"action":"7.14.0"},"references":[],"type":"action","updated_at":"2021-08-26T20:35:12.447Z","version":"WzM1ODQsMV0="} +{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T20:35:42.131Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"super description","external_service":{"connector_name":"A jira connector","external_id":"10125","external_title":"TPN-118","external_url":"https://cases-testing.atlassian.net/browse/TPN-118","pushed_at":"2021-08-26T20:35:44.302Z","pushed_by":{"email":null,"full_name":null,"username":"elastic"}},"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["other tags"],"title":"A case with a connector","type":"individual","updated_at":"2021-08-26T20:36:35.536Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases":"7.15.0"},"references":[{"id":"1cd34740-06ad-11ec-babc-0b08808e8e01","name":"pushConnectorId","type":"action"}],"type":"cases","updated_at":"2021-08-26T20:36:35.537Z","version":"WzM1OTIsMV0="} +{"attributes":{"action":"create","action_at":"2021-08-26T20:35:42.131Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case with a connector\",\"tags\":[\"other tags\"],\"description\":\"super description\",\"connector\":{\"id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"name\":\"A jira connector\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10002\",\"parent\":null,\"priority\":\"High\"}},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"2e9db8c0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010142131,4024],"type":"cases-user-actions","updated_at":"2021-08-26T20:35:42.284Z","version":"WzM1ODksMV0="} +{"attributes":{"action":"push-to-service","action_at":"2021-08-26T20:35:44.302Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["pushed"],"new_value":"{\"pushed_at\":\"2021-08-26T20:35:44.302Z\",\"pushed_by\":{\"username\":\"elastic\",\"full_name\":null,\"email\":null},\"connector_id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"connector_name\":\"A jira connector\",\"external_id\":\"10125\",\"external_title\":\"TPN-118\",\"external_url\":\"https://cases-testing.atlassian.net/browse/TPN-118\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"2fd1cbf0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010144302,4029],"type":"cases-user-actions","updated_at":"2021-08-26T20:35:44.303Z","version":"WzM1OTAsMV0="} +{"attributes":{"action":"update","action_at":"2021-08-26T20:36:35.536Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["connector"],"new_value":"{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null}","old_value":"{\"id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"name\":\"A jira connector\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10002\",\"parent\":null,\"priority\":\"High\"}}","owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"4ee9b250-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010195536,4033],"type":"cases-user-actions","updated_at":"2021-08-26T20:36:36.469Z","version":"WzM1OTMsMV0="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":5,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts new file mode 100644 index 0000000000000..df4e858e8a290 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { join } from 'path'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + deleteAllCaseItems, + createCase, + createComment, + findCases, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + AttributesTypeUser, + CommentsResponse, + CASES_URL, + CaseType, +} from '../../../../../../plugins/cases/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('import and export cases', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteAllCaseItems(es); + await actionsRemover.removeAll(); + }); + + it('exports a case with its associated user actions and comments', async () => { + const caseRequest = getPostCaseRequest(); + const postedCase = await createCase(supertest, caseRequest); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const { text } = await supertest + .post(`/api/saved_objects/_export`) + .send({ + type: ['cases'], + excludeExportDetails: true, + includeReferencesDeep: true, + }) + .set('kbn-xsrf', 'true'); + + const objects = ndjsonToObject(text); + + expect(objects).to.have.length(4); + + // should be the case + expect(objects[0].attributes.title).to.eql(caseRequest.title); + expect(objects[0].attributes.description).to.eql(caseRequest.description); + expect(objects[0].attributes.connector.type).to.eql(caseRequest.connector.type); + expect(objects[0].attributes.connector.name).to.eql(caseRequest.connector.name); + expect(objects[0].attributes.connector.fields).to.eql([]); + expect(objects[0].attributes.settings).to.eql(caseRequest.settings); + + // should be two user actions + expect(objects[1].attributes.action).to.eql('create'); + + const parsedCaseNewValue = JSON.parse(objects[1].attributes.new_value); + const { + connector: { id: ignoreParsedId, ...restParsedConnector }, + ...restParsedCreateCase + } = parsedCaseNewValue; + + const { + connector: { id: ignoreConnectorId, ...restConnector }, + ...restCreateCase + } = caseRequest; + + expect(restParsedCreateCase).to.eql({ ...restCreateCase, type: CaseType.individual }); + expect(restParsedConnector).to.eql(restConnector); + + expect(objects[1].attributes.old_value).to.eql(null); + expect(includesAllRequiredFields(objects[1].attributes.action_field)).to.eql(true); + + // should be the comment + expect(objects[2].attributes.comment).to.eql(postCommentUserReq.comment); + expect(objects[2].attributes.type).to.eql(postCommentUserReq.type); + + expect(objects[3].attributes.action).to.eql('create'); + expect(JSON.parse(objects[3].attributes.new_value)).to.eql(postCommentUserReq); + expect(objects[3].attributes.old_value).to.eql(null); + expect(objects[3].attributes.action_field).to.eql(['comment']); + }); + + it('imports a case with a comment and user actions', async () => { + await supertest + .post('/api/saved_objects/_import') + .query({ overwrite: true }) + .attach( + 'file', + join( + __dirname, + '../../../../common/fixtures/saved_object_exports/single_case_user_actions_one_comment.ndjson' + ) + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const findResponse = await findCases({ supertest, query: {} }); + expect(findResponse.total).to.eql(1); + expect(findResponse.cases[0].title).to.eql('A case to export'); + expect(findResponse.cases[0].description).to.eql('a description'); + + const { body: commentsResponse }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${findResponse.cases[0].id}/comments/_find`) + .send() + .expect(200); + + const comment = (commentsResponse.comments[0] as unknown) as AttributesTypeUser; + expect(comment.comment).to.eql('A comment for my case'); + + const userActions = await getCaseUserActions({ + supertest, + caseID: findResponse.cases[0].id, + }); + + expect(userActions).to.have.length(2); + expect(userActions[0].action).to.eql('create'); + expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); + + expect(userActions[1].action).to.eql('create'); + expect(userActions[1].action_field).to.eql(['comment']); + expect(userActions[1].old_value).to.eql(null); + expect(JSON.parse(userActions[1].new_value!)).to.eql({ + comment: 'A comment for my case', + type: 'user', + owner: 'securitySolution', + }); + }); + + it('imports a case with a connector', async () => { + await supertest + .post('/api/saved_objects/_import') + .query({ overwrite: true }) + .attach( + 'file', + join( + __dirname, + '../../../../common/fixtures/saved_object_exports/single_case_with_connector_update_to_none.ndjson' + ) + ) + .set('kbn-xsrf', 'true') + .expect(200); + + actionsRemover.add('default', '1cd34740-06ad-11ec-babc-0b08808e8e01', 'action', 'actions'); + + const findResponse = await findCases({ supertest, query: {} }); + expect(findResponse.total).to.eql(1); + expect(findResponse.cases[0].title).to.eql('A case with a connector'); + expect(findResponse.cases[0].description).to.eql('super description'); + + const userActions = await getCaseUserActions({ + supertest, + caseID: findResponse.cases[0].id, + }); + + expect(userActions).to.have.length(3); + expect(userActions[0].action).to.eql('create'); + expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); + + expect(userActions[1].action).to.eql('push-to-service'); + expect(userActions[1].action_field).to.eql(['pushed']); + expect(userActions[1].old_value).to.eql(null); + + const parsedPushNewValue = JSON.parse(userActions[1].new_value!); + expect(parsedPushNewValue.connector_name).to.eql('A jira connector'); + expect(parsedPushNewValue.connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); + + expect(userActions[2].action).to.eql('update'); + expect(userActions[2].action_field).to.eql(['connector']); + + const parsedUpdateNewValue = JSON.parse(userActions[2].new_value!); + expect(parsedUpdateNewValue.id).to.eql('none'); + }); + }); +}; + +const ndjsonToObject = (input: string) => { + return input.split('\n').map((str) => JSON.parse(str)); +}; + +const includesAllRequiredFields = (actionFields: string[]): boolean => { + const requiredFields = [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ]; + + return requiredFields.every((field) => actionFields.includes(field)); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index 9b24de26245f4..fba60634cc3d7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -20,6 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./alerts/get_cases')); loadTestFile(require.resolve('./alerts/get_alerts_attached_to_case')); loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/import_export')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); From 93e8a7d91b6c2f59c7097cbdd2df78e10e864c75 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 14 Sep 2021 11:15:53 -0700 Subject: [PATCH 31/39] [Reporting] Add config settings to telemetry for usage analysis (#112035) --- x-pack/plugins/reporting/server/config/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index d0c743f859b3c..119f49df014e2 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -54,4 +54,16 @@ export const config: PluginConfigDescriptor = { } }, ], + exposeToUsage: { + capture: { + maxAttempts: true, + timeouts: { openUrl: true, renderComplete: true, waitForElements: true }, + networkPolicy: false, // show as [redacted] + zoom: true, + }, + csv: { maxSizeBytes: true, scroll: { size: true, duration: true } }, + kibanaServer: false, // show as [redacted] + queue: { indexInterval: true, pollEnabled: true, timeout: true }, + roles: { enabled: true }, + }, }; From cdb9fbac83d50edb97861c68cd873eb784590238 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Tue, 14 Sep 2021 14:41:29 -0400 Subject: [PATCH 32/39] [RAC][Security Solution] Adds Threshold rule type and removes reliance on outputIndex (#111437) * Initial commit * Properly handle signal history * Fix #95258 - cardinality sort bug * Init threshold rule * Create working threshold rule * Fix threshold signal generation * Fix tests * Update mappings * ALERT_TYPE_ID => RULE_TYPE_ID * Add tests * Fix types --- .../security_solution/common/constants.ts | 1 - .../rule_types/__mocks__/threshold.ts | 104 ++++++++++++++++++ .../factories/bulk_create_factory.ts | 16 ++- .../factories/utils/build_alert.test.ts | 2 - .../rule_types/factories/utils/build_alert.ts | 2 +- .../factories/utils/flatten_with_prefix.ts | 20 +++- .../rule_types/field_maps/alerts.ts | 87 +-------------- .../rule_types/field_maps/field_names.ts | 5 +- .../rule_types/field_maps/rules.ts | 66 +++++++++-- .../lib/detection_engine/rule_types/index.ts | 3 +- .../query/create_query_alert_type.test.ts | 2 +- .../scripts/create_rule_threshold.sh | 63 +++++++++++ .../create_threshold_alert_type.test.ts | 34 ++++++ .../threshold/create_threshold_alert_type.ts | 95 ++++++++++++++++ .../detection_engine/rules/find_rules.test.ts | 8 +- .../detection_engine/schemas/rule_schemas.ts | 7 +- .../signals/executors/threshold.test.ts | 1 + .../signals/executors/threshold.ts | 88 +++++++++++---- .../signals/signal_rule_alert_type.ts | 4 +- .../signals/single_search_after.ts | 2 + .../threshold/build_signal_history.test.ts | 33 ++++++ .../signals/threshold/build_signal_history.ts | 75 +++++++++++++ .../bulk_create_threshold_signals.ts | 12 +- .../threshold/find_threshold_signals.test.ts | 2 + .../threshold/find_threshold_signals.ts | 9 ++ .../get_threshold_bucket_filters.test.ts | 2 +- .../threshold/get_threshold_bucket_filters.ts | 12 +- .../threshold/get_threshold_signal_history.ts | 52 +-------- .../lib/detection_engine/signals/types.ts | 5 + .../lib/detection_engine/signals/utils.ts | 6 +- .../security_solution/server/plugin.ts | 4 +- 31 files changed, 625 insertions(+), 197 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 8076caf60f697..a93439b29069b 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -197,7 +197,6 @@ export const EQL_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.eqlRule` as const; export const INDICATOR_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.indicatorRule` as const; export const ML_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.mlRule` as const; export const QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.queryRule` as const; -export const SAVED_QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.savedQueryRule` as const; export const THRESHOLD_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.thresholdRule` as const; /** diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts index 40d2ed37a5576..554672806c12e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts @@ -5,7 +5,35 @@ * 2.0. */ +import { + SPACE_IDS, + ALERT_RULE_CONSUMER, + ALERT_REASON, + ALERT_STATUS, + ALERT_STATUS_ACTIVE, + ALERT_WORKFLOW_STATUS, + ALERT_RULE_NAMESPACE, + ALERT_INSTANCE_ID, + ALERT_UUID, + ALERT_RULE_TYPE_ID, + ALERT_RULE_PRODUCER, + ALERT_RULE_CATEGORY, + ALERT_RULE_UUID, + ALERT_RULE_NAME, +} from '@kbn/rule-data-utils'; +import { TypeOfFieldMap } from '../../../../../../rule_registry/common/field_map'; +import { SERVER_APP_ID } from '../../../../../common/constants'; +import { ANCHOR_DATE } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; +import { flattenWithPrefix } from '../factories/utils/flatten_with_prefix'; +import { RulesFieldMap } from '../field_maps'; +import { + ALERT_ANCESTORS, + ALERT_ORIGINAL_TIME, + ALERT_ORIGINAL_EVENT, +} from '../field_maps/field_names'; +import { WrappedRACAlert } from '../types'; export const mockThresholdResults = { rawResponse: { @@ -59,3 +87,79 @@ export const mockThresholdResults = { }, }, }; + +export const sampleThresholdAlert: WrappedRACAlert = { + _id: 'b3ad77a4-65bd-4c4e-89cf-13c46f54bc4d', + _index: 'some-index', + _source: { + '@timestamp': '2020-04-20T21:26:30.000Z', + [SPACE_IDS]: ['default'], + [ALERT_INSTANCE_ID]: 'b3ad77a4-65bd-4c4e-89cf-13c46f54bc4d', + [ALERT_UUID]: '310158f7-994d-4a38-8cdc-152139ac4d29', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_ANCESTORS]: [ + { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], + [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z', + [ALERT_ORIGINAL_EVENT]: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + [ALERT_REASON]: 'alert reasonable reason', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + 'source.ip': '127.0.0.1', + 'host.name': 'garden-gnomes', + [ALERT_RULE_CATEGORY]: 'security', + [ALERT_RULE_NAME]: 'a threshold rule', + [ALERT_RULE_PRODUCER]: 'siem', + [ALERT_RULE_TYPE_ID]: 'query-rule-id', + [ALERT_RULE_UUID]: '151af49f-2e82-4b6f-831b-7f8cb341a5ff', + ...(flattenWithPrefix(ALERT_RULE_NAMESPACE, { + author: [], + uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: new Date(ANCHOR_DATE).toISOString(), + updated_at: new Date(ANCHOR_DATE).toISOString(), + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic_kibana', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + threat: [], + threshold: { + field: ['source.ip', 'host.name'], + value: 1, + }, + version: 1, + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + max_signals: 100, + risk_score: 55, + risk_score_mapping: [], + language: 'kuery', + rule_id: 'f88a544c-1d4e-4652-ae2a-c953b38da5d0', + interval: '5m', + exceptions_list: getListArrayMock(), + }) as TypeOfFieldMap), + 'kibana.alert.depth': 1, + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index c600e187bc8f1..cd7a5150ad384 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; + import { performance } from 'perf_hooks'; import { countBy, isEmpty } from 'lodash'; @@ -62,11 +64,15 @@ export const bulkCreateFactory = ( ); const createdItems = wrappedDocs - .map((doc, index) => ({ - _id: response.body.items[index].index?._id ?? '', - _index: response.body.items[index].index?._index ?? '', - ...doc._source, - })) + .map((doc, index) => { + const responseIndex = response.body.items[index].index; + return { + _id: responseIndex?._id ?? '', + _index: responseIndex?._index ?? '', + [ALERT_INSTANCE_ID]: responseIndex?._id ?? '', + ...doc._source, + }; + }) .filter((_, index) => response.body.items[index].index?.status === 201); const createdItemsCount = createdItems.length; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index 9be18a1a13453..453c4d8b785d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -102,7 +102,6 @@ describe('buildAlert', () => { status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', - output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], @@ -179,7 +178,6 @@ describe('buildAlert', () => { status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', - output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 5fbca2dc6178a..d39d6fa2eb805 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -120,7 +120,7 @@ export const buildAlert = ( [] ); - const { id, ...mappedRule } = rule; + const { id, output_index: outputIndex, ...mappedRule } = rule; mappedRule.uuid = id; return ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts index d472dc5885e57..02f418a151888 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts @@ -5,16 +5,26 @@ * 2.0. */ +import { isPlainObject } from 'lodash'; import { SearchTypes } from '../../../../../../common/detection_engine/types'; export const flattenWithPrefix = ( prefix: string, - obj: Record + maybeObj: unknown ): Record => { - return Object.keys(obj).reduce((acc: Record, key) => { + if (maybeObj != null && isPlainObject(maybeObj)) { + return Object.keys(maybeObj as Record).reduce( + (acc: Record, key) => { + return { + ...acc, + ...flattenWithPrefix(`${prefix}.${key}`, (maybeObj as Record)[key]), + }; + }, + {} + ); + } else { return { - ...acc, - [`${prefix}.${key}`]: obj[key], + [prefix]: maybeObj as SearchTypes, }; - }, {}); + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 1c4b7f03fd73f..f21fc5b6ad393 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -43,11 +43,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, - 'kibana.alert.group': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.group.id': { type: 'keyword', array: false, @@ -58,11 +53,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: false, }, - 'kibana.alert.original_event': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.original_event.action': { type: 'keyword', array: false, @@ -198,81 +188,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: false, }, - 'kibana.alert.threat': { - type: 'object', - array: false, - required: false, - }, - 'kibana.alert.threat.framework': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threshold_result': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.threshold_result.cardinality': { type: 'object', array: false, @@ -300,7 +215,7 @@ export const alertsFieldMap: FieldMap = { }, 'kibana.alert.threshold_result.terms': { type: 'object', - array: false, + array: true, required: false, }, 'kibana.alert.threshold_result.terms.field': { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts index fb9e597a30448..68d08e08086a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_NAMESPACE } from '@kbn/rule-data-utils'; +import { ALERT_NAMESPACE, ALERT_RULE_NAMESPACE } from '@kbn/rule-data-utils'; export const ALERT_ANCESTORS = `${ALERT_NAMESPACE}.ancestors` as const; export const ALERT_BUILDING_BLOCK_TYPE = `${ALERT_NAMESPACE}.building_block_type` as const; @@ -14,3 +14,6 @@ export const ALERT_GROUP_ID = `${ALERT_NAMESPACE}.group.id` as const; export const ALERT_GROUP_INDEX = `${ALERT_NAMESPACE}.group.index` as const; export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event` as const; export const ALERT_ORIGINAL_TIME = `${ALERT_NAMESPACE}.original_time` as const; + +const ALERT_RULE_THRESHOLD = `${ALERT_RULE_NAMESPACE}.threshold` as const; +export const ALERT_RULE_THRESHOLD_FIELD = `${ALERT_RULE_THRESHOLD}.field` as const; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts index 21405672fdf7f..87b55e092ec5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts @@ -11,6 +11,11 @@ export const rulesFieldMap = { array: false, required: false, }, + 'kibana.alert.rule.exceptions_list': { + type: 'object', + array: true, + required: false, + }, 'kibana.alert.rule.false_positives': { type: 'keyword', array: true, @@ -46,6 +51,56 @@ export const rulesFieldMap = { array: true, required: true, }, + 'kibana.alert.rule.threat.framework': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.reference': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.reference': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.reference': { + type: 'keyword', + array: false, + required: true, + }, 'kibana.alert.rule.threat_filters': { type: 'keyword', array: true, @@ -91,11 +146,6 @@ export const rulesFieldMap = { array: true, required: false, }, - 'kibana.alert.rule.threshold': { - type: 'object', - array: true, - required: false, - }, 'kibana.alert.rule.threshold.field': { type: 'keyword', array: true, @@ -103,7 +153,7 @@ export const rulesFieldMap = { }, 'kibana.alert.rule.threshold.value': { type: 'float', // TODO: should be 'long' (eventually, after we stabilize) - array: true, + array: false, required: false, }, 'kibana.alert.rule.threshold.cardinality': { @@ -113,12 +163,12 @@ export const rulesFieldMap = { }, 'kibana.alert.rule.threshold.cardinality.field': { type: 'keyword', - array: true, + array: false, required: false, }, 'kibana.alert.rule.threshold.cardinality.value': { type: 'long', - array: true, + array: false, required: false, }, 'kibana.alert.rule.timeline_id': { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts index 39325cab2c762..1787a15588b51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts @@ -7,5 +7,6 @@ export { createEqlAlertType } from './eql/create_eql_alert_type'; export { createIndicatorMatchAlertType } from './indicator_match/create_indicator_match_alert_type'; -export { createQueryAlertType } from './query/create_query_alert_type'; export { createMlAlertType } from './ml/create_ml_alert_type'; +export { createQueryAlertType } from './query/create_query_alert_type'; +export { createThresholdAlertType } from './threshold/create_threshold_alert_type'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index ed791af08890c..e45d8440386fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -24,7 +24,7 @@ jest.mock('../utils/get_list_client', () => ({ jest.mock('../../rule_execution_log/rule_execution_log_client'); -describe('Custom query alerts', () => { +describe('Custom Query Alerts', () => { it('does not send an alert when no events found', async () => { const { services, dependencies, executor } = createRuleTypeMocks(); const queryAlertType = createQueryAlertType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh new file mode 100644 index 0000000000000..47c5cb4eda2e9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "author": [], + "description": "Basic threshold rule", + "exceptionsList": [], + "falsePositives": [], + "from": "now-300s", + "query": "*:*", + "immutable": false, + "index": ["*"], + "language": "kuery", + "maxSignals": 10, + "outputIndex": "", + "references": [], + "riskScore": 21, + "riskScoreMapping": [], + "ruleId": "52dec1ba-b779-469c-9667-6b0e865fb89a", + "severity": "low", + "severityMapping": [], + "threat": [], + "threshold": { + "field": ["source.ip"], + "value": 2, + "cardinality": [ + { + "field": "source.ip", + "value": 1 + } + ] + }, + "to": "now", + "type": "threshold", + "version": 1 + }, + "consumer":"alerts", + "alertTypeId":"siem.thresholdRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "custom", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic threshold rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts new file mode 100644 index 0000000000000..74435cb300472 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { createThresholdAlertType } from './create_threshold_alert_type'; +import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; + +jest.mock('../../rule_execution_log/rule_execution_log_client'); + +describe('Threshold Alerts', () => { + it('does not send an alert when no events found', async () => { + const params = getThresholdRuleParams(); + const { dependencies, executor } = createRuleTypeMocks('threshold', params); + const thresholdAlertTpe = createThresholdAlertType({ + experimentalFeatures: allowedExperimentalValues, + lists: dependencies.lists, + logger: dependencies.logger, + mergeStrategy: 'allFields', + ignoreFields: [], + ruleDataClient: dependencies.ruleDataClient, + ruleDataService: dependencies.ruleDataService, + version: '1.0.0', + }); + dependencies.alerting.registerType(thresholdAlertTpe); + + await executor({ params }); + expect(dependencies.ruleDataClient.getWriter).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts new file mode 100644 index 0000000000000..a503cf5aedbea --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; + +import { PersistenceServices } from '../../../../../../rule_registry/server'; +import { THRESHOLD_RULE_TYPE_ID } from '../../../../../common/constants'; +import { thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas'; +import { thresholdExecutor } from '../../signals/executors/threshold'; +import { ThresholdAlertState } from '../../signals/types'; +import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; +import { CreateRuleOptions } from '../types'; + +export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { + const { + experimentalFeatures, + lists, + logger, + mergeStrategy, + ignoreFields, + ruleDataClient, + version, + ruleDataService, + } = createOptions; + const createSecurityRuleType = createSecurityRuleTypeFactory({ + lists, + logger, + mergeStrategy, + ignoreFields, + ruleDataClient, + ruleDataService, + }); + return createSecurityRuleType({ + id: THRESHOLD_RULE_TYPE_ID, + name: 'Threshold Rule', + validate: { + params: { + validate: (object: unknown): ThresholdRuleParams => { + const [validated, errors] = validateNonExact(object, thresholdRuleParams); + if (errors != null) { + throw new Error(errors); + } + if (validated == null) { + throw new Error('Validation of rule params failed'); + } + return validated; + }, + }, + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + producer: 'security-solution', + async executor(execOptions) { + const { + runOpts: { buildRuleMessage, bulkCreate, exceptionItems, rule, tuple, wrapHits }, + services, + startedAt, + state, + } = execOptions; + + // console.log(JSON.stringify(state)); + + const result = await thresholdExecutor({ + buildRuleMessage, + bulkCreate, + exceptionItems, + experimentalFeatures, + logger, + rule, + services, + startedAt, + state, + tuple, + version, + wrapHits, + }); + + return result; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts index 0f7545c4df936..ebde1d0ad6df8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts @@ -7,15 +7,19 @@ import { getFilter } from './find_rules'; import { + EQL_RULE_TYPE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, SIGNALS_ID, } from '../../../../common/constants'; -const allAlertTypeIds = `(alert.attributes.alertTypeId: ${ML_RULE_TYPE_ID} +const allAlertTypeIds = `(alert.attributes.alertTypeId: ${EQL_RULE_TYPE_ID} + OR alert.attributes.alertTypeId: ${ML_RULE_TYPE_ID} OR alert.attributes.alertTypeId: ${QUERY_RULE_TYPE_ID} - OR alert.attributes.alertTypeId: ${INDICATOR_RULE_TYPE_ID})`.replace(/[\n\r]/g, ''); + OR alert.attributes.alertTypeId: ${INDICATOR_RULE_TYPE_ID} + OR alert.attributes.alertTypeId: ${THRESHOLD_RULE_TYPE_ID})`.replace(/[\n\r]/g, ''); describe('find_rules', () => { const fullFilterTestCases: Array<[boolean, string]> = [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index e9215084614c0..578d8c4926b69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -69,6 +69,8 @@ import { INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, + EQL_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, } from '../../../../common/constants'; const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); @@ -206,12 +208,11 @@ export const notifyWhen = t.union([ export const allRuleTypes = t.union([ t.literal(SIGNALS_ID), - // t.literal(EQL_RULE_TYPE_ID), + t.literal(EQL_RULE_TYPE_ID), t.literal(ML_RULE_TYPE_ID), t.literal(QUERY_RULE_TYPE_ID), - // t.literal(SAVED_QUERY_RULE_TYPE_ID), t.literal(INDICATOR_RULE_TYPE_ID), - // t.literal(THRESHOLD_RULE_TYPE_ID), + t.literal(THRESHOLD_RULE_TYPE_ID), ]); export type AllRuleTypes = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index afcb3707591fc..5766390099e29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -73,6 +73,7 @@ describe('threshold_executor', () => { exceptionItems, experimentalFeatures: allowedExperimentalValues, services: alertServices, + state: { initialized: true, signalHistory: {} }, version, logger, buildRuleMessage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index ffd90f3b90b91..524bc6a6c524c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -5,9 +5,12 @@ * 2.0. */ +import { SearchHit } from '@elastic/elasticsearch/api/types'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { Logger } from 'src/core/server'; import { SavedObject } from 'src/core/types'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { AlertInstanceContext, AlertInstanceState, @@ -28,6 +31,7 @@ import { BulkCreate, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, + ThresholdAlertState, WrapHits, } from '../types'; import { @@ -37,6 +41,7 @@ import { } from '../utils'; import { BuildRuleMessage } from '../rule_messages'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildThresholdSignalHistory } from '../threshold/build_signal_history'; export const thresholdExecutor = async ({ rule, @@ -48,6 +53,7 @@ export const thresholdExecutor = async ({ logger, buildRuleMessage, startedAt, + state, bulkCreate, wrapHits, }: { @@ -60,17 +66,48 @@ export const thresholdExecutor = async ({ logger: Logger; buildRuleMessage: BuildRuleMessage; startedAt: Date; + state: ThresholdAlertState; bulkCreate: BulkCreate; wrapHits: WrapHits; -}): Promise => { +}): Promise => { let result = createSearchAfterReturnType(); const ruleParams = rule.attributes.params; + + // Get state or build initial state (on upgrade) + const { signalHistory, searchErrors: previousSearchErrors } = state.initialized + ? { signalHistory: state.signalHistory, searchErrors: [] } + : await getThresholdSignalHistory({ + indexPattern: ['*'], // TODO: get outputIndex? + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + ruleId: ruleParams.ruleId, + bucketByFields: ruleParams.threshold.field, + timestampOverride: ruleParams.timestampOverride, + buildRuleMessage, + }); + + if (!state.initialized) { + // Clean up any signal history that has fallen outside the window + const toDelete: string[] = []; + for (const [hash, entry] of Object.entries(signalHistory)) { + if (entry.lastSignalTimestamp < tuple.from.valueOf()) { + toDelete.push(hash); + } + } + for (const hash of toDelete) { + delete signalHistory[hash]; + } + } + if (hasLargeValueItem(exceptionItems)) { result.warningMessages.push( 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' ); result.warning = true; } + const inputIndex = await getInputIndex({ experimentalFeatures, services, @@ -78,23 +115,8 @@ export const thresholdExecutor = async ({ index: ruleParams.index, }); - const { - thresholdSignalHistory, - searchErrors: previousSearchErrors, - } = await getThresholdSignalHistory({ - indexPattern: [ruleParams.outputIndex], - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - services, - logger, - ruleId: ruleParams.ruleId, - bucketByFields: ruleParams.threshold.field, - timestampOverride: ruleParams.timestampOverride, - buildRuleMessage, - }); - const bucketFilters = await getThresholdBucketFilters({ - thresholdSignalHistory, + signalHistory, timestampOverride: ruleParams.timestampOverride, }); @@ -141,7 +163,7 @@ export const thresholdExecutor = async ({ signalsIndex: ruleParams.outputIndex, startedAt, from: tuple.from.toDate(), - thresholdSignalHistory, + signalHistory, bulkCreate, wrapHits, }); @@ -161,5 +183,31 @@ export const thresholdExecutor = async ({ searchAfterTimes: [thresholdSearchDuration], }), ]); - return result; + + const createdAlerts = createdItems.map((alert) => { + const { _id, _index, ...source } = alert as { _id: string; _index: string }; + return { + _id, + _index, + _source: { + ...source, + }, + } as SearchHit; + }); + + const newSignalHistory = buildThresholdSignalHistory({ + alerts: createdAlerts, + }); + + return { + ...result, + state: { + ...state, + initialized: true, + signalHistory: { + ...signalHistory, + ...newSignalHistory, + }, + }, + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 68d60f7757e4a..9a6c099ed1760 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -29,7 +29,7 @@ import { } from '../../../../common/detection_engine/utils'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; -import { AlertAttributes, SignalRuleAlertTypeDefinition } from './types'; +import { AlertAttributes, SignalRuleAlertTypeDefinition, ThresholdAlertState } from './types'; import { getListsClient, getExceptions, @@ -125,6 +125,7 @@ export const signalRulesAlertType = ({ async executor({ previousStartedAt, startedAt, + state, alertId, services, params, @@ -316,6 +317,7 @@ export const signalRulesAlertType = ({ logger, buildRuleMessage, startedAt, + state: state as ThresholdAlertState, bulkCreate, wrapHits, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index ff49fb5892f50..7b30f70701e8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -75,9 +75,11 @@ export const singleSearchAfter = async ({ searchAfterQuery as estypes.SearchRequest ); const end = performance.now(); + const searchErrors = createErrorsFromShard({ errors: nextSearchAfterResult._shards.failures ?? [], }); + return { searchResult: nextSearchAfterResult, searchDuration: makeFloatString(end - start), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts new file mode 100644 index 0000000000000..8362942af15b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALERT_ORIGINAL_TIME } from '../../rule_types/field_maps/field_names'; +import { sampleThresholdAlert } from '../../rule_types/__mocks__/threshold'; +import { buildThresholdSignalHistory } from './build_signal_history'; + +describe('buildSignalHistory', () => { + it('builds a signal history from an alert', () => { + const signalHistory = buildThresholdSignalHistory({ alerts: [sampleThresholdAlert] }); + expect(signalHistory).toEqual({ + '7a75c5c2db61f57ec166c669cb8244b91f812f0b2f1d4f8afd528d4f8b4e199b': { + lastSignalTimestamp: Date.parse( + sampleThresholdAlert._source[ALERT_ORIGINAL_TIME] as string + ), + terms: [ + { + field: 'host.name', + value: 'garden-gnomes', + }, + { + field: 'source.ip', + value: '127.0.0.1', + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts new file mode 100644 index 0000000000000..81b12d2d4f229 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchHit } from '@elastic/elasticsearch/api/types'; +import { + ALERT_ORIGINAL_TIME, + ALERT_RULE_THRESHOLD_FIELD, +} from '../../rule_types/field_maps/field_names'; + +import { SimpleHit, ThresholdSignalHistory } from '../types'; +import { getThresholdTermsHash, isWrappedRACAlert, isWrappedSignalHit } from '../utils'; + +interface GetThresholdSignalHistoryParams { + alerts: Array>; +} + +const getTerms = (alert: SimpleHit) => { + if (isWrappedRACAlert(alert)) { + return (alert._source[ALERT_RULE_THRESHOLD_FIELD] as string[]).map((field) => ({ + field, + value: alert._source[field] as string, + })); + } else if (isWrappedSignalHit(alert)) { + return alert._source.signal?.threshold_result?.terms ?? []; + } else { + // We shouldn't be here + return []; + } +}; + +const getOriginalTime = (alert: SimpleHit) => { + if (isWrappedRACAlert(alert)) { + const originalTime = alert._source[ALERT_ORIGINAL_TIME]; + return originalTime != null ? new Date(originalTime as string).getTime() : undefined; + } else if (isWrappedSignalHit(alert)) { + const originalTime = alert._source.signal?.original_time; + return originalTime != null ? new Date(originalTime).getTime() : undefined; + } else { + // We shouldn't be here + return undefined; + } +}; + +export const buildThresholdSignalHistory = ({ + alerts, +}: GetThresholdSignalHistoryParams): ThresholdSignalHistory => { + const signalHistory = alerts.reduce((acc, alert) => { + if (!alert._source) { + return acc; + } + + const terms = getTerms(alert as SimpleHit); + const hash = getThresholdTermsHash(terms); + const existing = acc[hash]; + const originalTime = getOriginalTime(alert as SimpleHit); + + if (existing != null) { + if (originalTime && originalTime > existing.lastSignalTimestamp) { + acc[hash].lastSignalTimestamp = originalTime; + } + } else if (originalTime) { + acc[hash] = { + terms, + lastSignalTimestamp: originalTime, + }; + } + return acc; + }, {}); + + return signalHistory; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index afb0353c4ba03..ce8ee4542d603 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -46,7 +46,7 @@ interface BulkCreateThresholdSignalsParams { signalsIndex: string; startedAt: Date; from: Date; - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; bulkCreate: BulkCreate; wrapHits: WrapHits; } @@ -61,7 +61,7 @@ const getTransformedHits = ( ruleId: string, filter: unknown, timestampOverride: TimestampOverrideOrUndefined, - thresholdSignalHistory: ThresholdSignalHistory + signalHistory: ThresholdSignalHistory ) => { const aggParts = threshold.field.length ? results.aggregations && getThresholdAggregationParts(results.aggregations) @@ -148,7 +148,7 @@ const getTransformedHits = ( } const termsHash = getThresholdTermsHash(bucket.terms); - const signalHit = thresholdSignalHistory[termsHash]; + const signalHit = signalHistory[termsHash]; const source = { '@timestamp': timestamp, @@ -202,7 +202,7 @@ export const transformThresholdResultsToEcs = ( threshold: ThresholdNormalized, ruleId: string, timestampOverride: TimestampOverrideOrUndefined, - thresholdSignalHistory: ThresholdSignalHistory + signalHistory: ThresholdSignalHistory ): SignalSearchResponse => { const transformedHits = getTransformedHits( results, @@ -214,7 +214,7 @@ export const transformThresholdResultsToEcs = ( ruleId, filter, timestampOverride, - thresholdSignalHistory + signalHistory ); const thresholdResults = { ...results, @@ -246,7 +246,7 @@ export const bulkCreateThresholdSignals = async ( ruleParams.threshold, ruleParams.ruleId, ruleParams.timestampOverride, - params.thresholdSignalHistory + params.signalHistory ); return params.bulkCreate( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts index e84b4f31fb15f..41d46925770bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts @@ -227,6 +227,7 @@ describe('findThresholdSignals', () => { 'threshold_1:user.name': { terms: { field: 'user.name', + order: { cardinality_count: 'desc' }, min_doc_count: 100, size: 10000, }, @@ -302,6 +303,7 @@ describe('findThresholdSignals', () => { lang: 'painless', }, min_doc_count: 200, + order: { cardinality_count: 'desc' }, }, aggs: { cardinality_count: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index ca7f22e4a7570..740ba281cfcfb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -89,6 +89,13 @@ export const findThresholdSignals = async ({ const thresholdFields = threshold.field; + // order buckets by cardinality (https://github.com/elastic/kibana/issues/95258) + const thresholdFieldCount = thresholdFields.length; + const orderByCardinality = (i: number = 0) => + (thresholdFieldCount === 0 || i === thresholdFieldCount - 1) && threshold.cardinality?.length + ? { order: { cardinality_count: 'desc' } } + : {}; + // Generate a nested terms aggregation for each threshold grouping field provided, appending leaf // aggregations to 1) filter out buckets that don't meet the cardinality threshold, if provided, and // 2) return the latest hit for each bucket so that we can persist the timestamp of the event in the @@ -104,6 +111,7 @@ export const findThresholdSignals = async ({ set(acc, aggPath, { terms: { field, + ...orderByCardinality(i), min_doc_count: threshold.value, // not needed on parent agg, but can help narrow down result set size: 10000, // max 10k buckets }, @@ -121,6 +129,7 @@ export const findThresholdSignals = async ({ source: '""', // Group everything in the same bucket lang: 'painless', }, + ...orderByCardinality(), min_doc_count: threshold.value, }, aggs: leafAggs, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts index d621868a0956c..e67a6fa3dfa9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts @@ -11,7 +11,7 @@ import { getThresholdBucketFilters } from './get_threshold_bucket_filters'; describe('getThresholdBucketFilters', () => { it('should generate filters for threshold signal detection with dupe mitigation', async () => { const result = await getThresholdBucketFilters({ - thresholdSignalHistory: sampleThresholdSignalHistory(), + signalHistory: sampleThresholdSignalHistory(), timestampOverride: undefined, }); expect(result).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index e6a188a20b5d5..c4569ef9818d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -9,14 +9,18 @@ import { Filter } from 'src/plugins/data/common'; import { ESFilter } from '../../../../../../../../src/core/types/elasticsearch'; import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; +/* + * Returns a filter to exclude events that have already been included in a + * previous threshold signal. Uses the threshold signal history to achieve this. + */ export const getThresholdBucketFilters = async ({ - thresholdSignalHistory, + signalHistory, timestampOverride, }: { - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; timestampOverride: string | undefined; }): Promise => { - const filters = Object.values(thresholdSignalHistory).reduce( + const filters = Object.values(signalHistory).reduce( (acc: ESFilter[], bucket: ThresholdSignalHistoryRecord): ESFilter[] => { const filter = { bool: { @@ -24,6 +28,7 @@ export const getThresholdBucketFilters = async ({ { range: { [timestampOverride ?? '@timestamp']: { + // Timestamp of last event signaled on for this set of terms. lte: new Date(bucket.lastSignalTimestamp).toISOString(), }, }, @@ -32,6 +37,7 @@ export const getThresholdBucketFilters = async ({ }, } as ESFilter; + // Terms to filter events older than `lastSignalTimestamp`. bucket.terms.forEach((term) => { if (term.field != null) { (filter.bool!.filter as ESFilter[]).push({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts index b93d8423c0259..276431c3bc929 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { TimestampOverrideOrUndefined } from '../../../../../common/detection_engine/schemas/common/schemas'; import { AlertInstanceContext, @@ -16,7 +15,7 @@ import { Logger } from '../../../../../../../../src/core/server'; import { ThresholdSignalHistory } from '../types'; import { BuildRuleMessage } from '../rule_messages'; import { findPreviousThresholdSignals } from './find_previous_threshold_signals'; -import { getThresholdTermsHash } from '../utils'; +import { buildThresholdSignalHistory } from './build_signal_history'; interface GetThresholdSignalHistoryParams { from: string; @@ -41,7 +40,7 @@ export const getThresholdSignalHistory = async ({ timestampOverride, buildRuleMessage, }: GetThresholdSignalHistoryParams): Promise<{ - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; searchErrors: string[]; }> => { const { searchResult, searchErrors } = await findPreviousThresholdSignals({ @@ -56,51 +55,10 @@ export const getThresholdSignalHistory = async ({ buildRuleMessage, }); - const thresholdSignalHistory = searchResult.hits.hits.reduce( - (acc, hit) => { - if (!hit._source) { - return acc; - } - - const terms = - hit._source.signal?.threshold_result?.terms != null - ? hit._source.signal.threshold_result.terms - : [ - // Pre-7.12 signals - { - field: - (((hit._source.signal?.rule as RulesSchema).threshold as unknown) as { - field: string; - }).field ?? '', - value: ((hit._source.signal?.threshold_result as unknown) as { value: string }) - .value, - }, - ]; - - const hash = getThresholdTermsHash(terms); - const existing = acc[hash]; - const originalTime = - hit._source.signal?.original_time != null - ? new Date(hit._source.signal?.original_time).getTime() - : undefined; - - if (existing != null) { - if (originalTime && originalTime > existing.lastSignalTimestamp) { - acc[hash].lastSignalTimestamp = originalTime; - } - } else if (originalTime) { - acc[hash] = { - terms, - lastSignalTimestamp: originalTime, - }; - } - return acc; - }, - {} - ); - return { - thresholdSignalHistory, + signalHistory: buildThresholdSignalHistory({ + alerts: searchResult.hits.hits, + }), searchErrors, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 3751f6f6e98f2..fc6b42c38549e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -362,3 +362,8 @@ export interface ThresholdQueryBucket extends TermAggregationBucket { value_as_string: string; }; } + +export interface ThresholdAlertState extends AlertTypeState { + initialized: boolean; + signalHistory: ThresholdSignalHistory; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index efd7200202b59..5993dd626729f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -63,10 +63,12 @@ import { WrappedRACAlert } from '../rule_types/types'; import { SearchTypes } from '../../../../common/detection_engine/types'; import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { + EQL_RULE_TYPE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, SIGNALS_ID, + THRESHOLD_RULE_TYPE_ID, } from '../../../../common/constants'; interface SortExceptionsReturn { @@ -1013,10 +1015,10 @@ export const getField = (event: SimpleHit, field: string) * Maps legacy rule types to RAC rule type IDs. */ export const ruleTypeMappings = { - eql: SIGNALS_ID, + eql: EQL_RULE_TYPE_ID, machine_learning: ML_RULE_TYPE_ID, query: QUERY_RULE_TYPE_ID, saved_query: SIGNALS_ID, threat_match: INDICATOR_RULE_TYPE_ID, - threshold: SIGNALS_ID, + threshold: THRESHOLD_RULE_TYPE_ID, }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f07137a118ab6..14da8ca650960 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -53,6 +53,7 @@ import { createIndicatorMatchAlertType, createMlAlertType, createQueryAlertType, + createThresholdAlertType, } from './lib/detection_engine/rule_types'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; @@ -264,9 +265,10 @@ export class Plugin implements IPlugin Date: Tue, 14 Sep 2021 20:56:05 +0200 Subject: [PATCH 33/39] [UX App] Add ux app no data screen (#111866) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/app/RumDashboard/RumHome.tsx | 31 ++++++++++++++++++- .../app/RumDashboard/hooks/useHasRumData.ts | 16 ++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useHasRumData.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index cb80698adeaa7..0371f7eb669e5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -16,22 +16,51 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { UxEnvironmentFilter } from '../../shared/EnvironmentFilter'; import { UserPercentile } from './UserPercentile'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; +import { KibanaPageTemplateProps } from '../../../../../../../src/plugins/kibana_react/public'; +import { useHasRumData } from './hooks/useHasRumData'; export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { defaultMessage: 'Dashboard', }); export function RumHome() { - const { observability } = useApmPluginContext(); + const { core, observability } = useApmPluginContext(); const PageTemplateComponent = observability.navigation.PageTemplate; const { isSmall, isXXL } = useBreakpoints(); + const { data: rumHasData } = useHasRumData(); + const envStyle = isSmall ? {} : { maxWidth: 500 }; + const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = !rumHasData?.hasData + ? { + solution: i18n.translate('xpack.apm.ux.overview.solutionName', { + defaultMessage: 'Observability', + }), + actions: { + beats: { + title: i18n.translate('xpack.apm.ux.overview.beatsCard.title', { + defaultMessage: 'Add RUM data', + }), + description: i18n.translate( + 'xpack.apm.ux.overview.beatsCard.description', + { + defaultMessage: + 'Use the RUM (JS) agent to collect user experience data.', + } + ), + href: core.http.basePath.prepend(`/app/home#/tutorial/apm`), + }, + }, + docsLink: core.docLinks.links.observability.guide, + } + : undefined; + return ( { + return callApmApi({ + endpoint: 'GET /api/apm/observability_overview/has_rum_data', + }); + }, []); +} From 07f4828f9a979f99b56359002bf21ad2393150d6 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Tue, 14 Sep 2021 15:28:08 -0400 Subject: [PATCH 34/39] [Stack Monitoring] convert server/logstash dir to typescript (#110706) * add types to logstash server dir * remove comment * fix type * remove unused type * add helper function to test * add issue to comment --- x-pack/plugins/monitoring/common/types/es.ts | 3 + .../lib/cluster/get_clusters_from_request.ts | 2 +- .../monitoring/server/lib/create_query.ts | 2 +- ...luster_status.js => get_cluster_status.ts} | 8 +- .../lib/logstash/get_logstash_for_clusters.ts | 2 +- .../server/lib/logstash/get_node_info.test.js | 177 -------------- .../server/lib/logstash/get_node_info.test.ts | 212 +++++++++++++++++ .../server/lib/logstash/get_node_info.ts | 3 - .../server/lib/logstash/get_nodes.ts | 4 - ...ipelines.js => get_paginated_pipelines.ts} | 217 ++++++++++++------ ..._pipeline.test.js => get_pipeline.test.ts} | 18 +- .../server/lib/logstash/get_pipeline.ts | 30 +-- .../server/lib/logstash/get_pipeline_ids.ts | 24 +- .../logstash/get_pipeline_state_document.ts | 27 +-- ...n.js => get_pipeline_stats_aggregation.ts} | 51 ++-- ...e_versions.js => get_pipeline_versions.ts} | 34 ++- .../lib/logstash/get_pipeline_vertex.ts | 32 +-- ... get_pipeline_vertex_stats_aggregation.ts} | 69 ++++-- .../{sort_pipelines.js => sort_pipelines.ts} | 8 +- .../lib/pagination/{filter.js => filter.ts} | 9 +- .../pagination/{paginate.js => paginate.ts} | 2 +- ....js => standalone_cluster_query_filter.ts} | 0 .../server/routes/api/v1/logstash/pipeline.js | 8 +- .../pipelines/cluster_pipeline_ids.js | 2 +- .../logstash/pipelines/cluster_pipelines.js | 11 +- .../v1/logstash/pipelines/node_pipelines.js | 11 +- x-pack/plugins/monitoring/server/types.ts | 74 ++++++ 27 files changed, 667 insertions(+), 373 deletions(-) rename x-pack/plugins/monitoring/server/lib/logstash/{get_cluster_status.js => get_cluster_status.ts} (87%) delete mode 100644 x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js create mode 100644 x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts rename x-pack/plugins/monitoring/server/lib/logstash/{get_paginated_pipelines.js => get_paginated_pipelines.ts} (51%) rename x-pack/plugins/monitoring/server/lib/logstash/{get_pipeline.test.js => get_pipeline.test.ts} (94%) rename x-pack/plugins/monitoring/server/lib/logstash/{get_pipeline_stats_aggregation.js => get_pipeline_stats_aggregation.ts} (80%) rename x-pack/plugins/monitoring/server/lib/logstash/{get_pipeline_versions.js => get_pipeline_versions.ts} (78%) rename x-pack/plugins/monitoring/server/lib/logstash/{get_pipeline_vertex_stats_aggregation.js => get_pipeline_vertex_stats_aggregation.ts} (75%) rename x-pack/plugins/monitoring/server/lib/logstash/{sort_pipelines.js => sort_pipelines.ts} (56%) rename x-pack/plugins/monitoring/server/lib/pagination/{filter.js => filter.ts} (75%) rename x-pack/plugins/monitoring/server/lib/pagination/{paginate.js => paginate.ts} (77%) rename x-pack/plugins/monitoring/server/lib/standalone_clusters/{standalone_cluster_query_filter.js => standalone_cluster_query_filter.ts} (100%) diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index f324164b09302..a9d020813ce84 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -228,6 +228,7 @@ export interface ElasticsearchLegacySource { }; queue?: { type?: string; + events?: number; }; jvm?: { uptime_in_millis?: number; @@ -249,6 +250,8 @@ export interface ElasticsearchLegacySource { }; events?: { out?: number; + in?: number; + filtered?: number; }; reloads?: { failures?: number; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts index a2b3434e6b3f7..9016f1916542b 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts @@ -190,7 +190,7 @@ export async function getClustersFromRequest( // add logstash data if (isInCodePath(codePaths, [CODE_PATH_LOGSTASH])) { const logstashes = await getLogstashForClusters(req, lsIndexPattern, clusters); - const pipelines = await getLogstashPipelineIds(req, lsIndexPattern, { clusterUuid }, 1); + const pipelines = await getLogstashPipelineIds({ req, lsIndexPattern, clusterUuid, size: 1 }); logstashes.forEach((logstash) => { const clusterIndex = clusters.findIndex( (cluster) => diff --git a/x-pack/plugins/monitoring/server/lib/create_query.ts b/x-pack/plugins/monitoring/server/lib/create_query.ts index 83817280730f2..8dead521d24fb 100644 --- a/x-pack/plugins/monitoring/server/lib/create_query.ts +++ b/x-pack/plugins/monitoring/server/lib/create_query.ts @@ -78,7 +78,7 @@ export function createQuery(options: { const isFromStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID; - let typeFilter; + let typeFilter: any; if (type) { typeFilter = { bool: { should: [{ term: { type } }, { term: { 'metricset.name': type } }] } }; } else if (types) { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.js b/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts similarity index 87% rename from x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts index 59ee4f9981bda..dfd1eaa155069 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_cluster_status.ts @@ -8,6 +8,7 @@ import { get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { getLogstashForClusters } from './get_logstash_for_clusters'; +import { LegacyRequest } from '../../types'; /** * Get the cluster status for Logstash instances. @@ -19,9 +20,12 @@ import { getLogstashForClusters } from './get_logstash_for_clusters'; * @param {String} clusterUuid The cluster UUID for the associated Elasticsearch cluster. * @returns {Promise} The cluster status object. */ -export function getClusterStatus(req, lsIndexPattern, { clusterUuid }) { +export function getClusterStatus( + req: LegacyRequest, + lsIndexPattern: string, + { clusterUuid }: { clusterUuid: string } +) { checkParam(lsIndexPattern, 'lsIndexPattern in logstash/getClusterStatus'); - const clusters = [{ cluster_uuid: clusterUuid }]; return getLogstashForClusters(req, lsIndexPattern, clusters).then((clusterStatus) => get(clusterStatus, '[0].stats') diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts index c0c29756818ee..480b7176b7aba 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts @@ -40,7 +40,7 @@ const getQueueTypes = (queueBuckets: Array ) { checkParam(lsIndexPattern, 'lsIndexPattern in logstash/getLogstashForClusters'); diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js deleted file mode 100644 index e34defc43afc8..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js +++ /dev/null @@ -1,177 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; -import { handleResponse, getNodeInfo } from './get_node_info'; -import { standaloneClusterFilter } from '../standalone_clusters/standalone_cluster_query_filter'; - -describe('get_logstash_info', () => { - // TODO: test was not running before and is not up to date - it.skip('return undefined for empty response', () => { - const result = handleResponse({}); - expect(result).toBe(undefined); - }); - - it('return mapped data for result with hits, availability = true', () => { - const result = handleResponse({ - hits: { - hits: [ - { - _source: { - logstash_stats: { - timestamp: moment().format(), - logstash: { - host: 'myhost', - }, - events: { - in: 300, - filtered: 300, - out: 300, - }, - reloads: { - successes: 5, - failures: 2, - }, - queue: { - type: 'persisted', - events: 100, - }, - }, - }, - }, - ], - }, - }); - expect(result).toEqual({ - host: 'myhost', - availability: true, - events: { - filtered: 300, - in: 300, - out: 300, - }, - reloads: { - successes: 5, - failures: 2, - }, - queue_type: 'persisted', - }); - }); - - it('return mapped data for result with hits, availability = false', () => { - const result = handleResponse({ - hits: { - hits: [ - { - _source: { - logstash_stats: { - timestamp: moment().subtract(11, 'minutes').format(), - logstash: { - host: 'myhost', - }, - events: { - in: 300, - filtered: 300, - out: 300, - }, - reloads: { - successes: 5, - failures: 2, - }, - queue: { - type: 'persisted', - events: 100, - }, - }, - }, - }, - ], - }, - }); - expect(result).toEqual({ - host: 'myhost', - availability: false, - events: { - filtered: 300, - in: 300, - out: 300, - }, - reloads: { - successes: 5, - failures: 2, - }, - queue_type: 'persisted', - }); - }); - - it('default to no queue type if none specified', () => { - const result = handleResponse({ - hits: { - hits: [ - { - _source: { - logstash_stats: { - timestamp: moment().subtract(11, 'minutes').format(), - logstash: { - host: 'myhost', - }, - events: { - in: 300, - filtered: 300, - out: 300, - }, - reloads: { - successes: 5, - failures: 2, - }, - }, - }, - }, - ], - }, - }); - expect(result).toEqual({ - host: 'myhost', - availability: false, - events: { - filtered: 300, - in: 300, - out: 300, - }, - reloads: { - successes: 5, - failures: 2, - }, - }); - }); - - it('works with standalone cluster', async () => { - const callWithRequest = jest.fn().mockReturnValue({ - then: jest.fn(), - }); - const req = { - server: { - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithRequest, - }), - }, - }, - }, - }; - await getNodeInfo(req, '.monitoring-logstash-*', { - clusterUuid: STANDALONE_CLUSTER_CLUSTER_UUID, - }); - expect(callWithRequest.mock.calls.length).toBe(1); - expect(callWithRequest.mock.calls[0].length).toBe(3); - expect(callWithRequest.mock.calls[0][2].body.query.bool.filter[0]).toBe( - standaloneClusterFilter - ); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts new file mode 100644 index 0000000000000..ea2eac7febb6d --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { set, unset } from 'lodash'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; +import { handleResponse, getNodeInfo } from './get_node_info'; +import { LegacyRequest } from '../../types'; +import { ElasticsearchResponseHit } from '../../../common/types/es'; +import { standaloneClusterFilter } from '../standalone_clusters/standalone_cluster_query_filter'; + +interface HitParams { + path: string; + value?: string; +} + +// deletes, adds, or updates the properties based on a default object +function createResponseObjHit(params?: HitParams[]): ElasticsearchResponseHit { + const defaultResponseObj: ElasticsearchResponseHit = { + _index: 'index', + _source: { + cluster_uuid: '123', + timestamp: '2021-08-31T15:00:26.330Z', + logstash_stats: { + timestamp: moment().format(), + logstash: { + pipeline: { + batch_size: 2, + workers: 2, + }, + host: 'myhost', + uuid: 'd63b22f8-7f77-4a23-9aac-9813c760e0e0', + version: '8.0.0', + status: 'green', + name: 'desktop-192-168-162-170.local', + http_address: '127.0.0.1:9600', + }, + events: { + in: 300, + filtered: 300, + out: 300, + }, + reloads: { + successes: 5, + failures: 2, + }, + queue: { + type: 'persisted', + events: 100, + }, + }, + }, + }; + + if (!params) return defaultResponseObj; + return params.reduce((acc, change) => { + if (!change.value) { + // delete if no value provided + unset(acc, change.path); + return acc; + } + return set(acc, change.path, change.value); + }, defaultResponseObj); +} + +const createResponseFromHits = (hits: ElasticsearchResponseHit[]) => { + return { + hits: { + total: { + value: hits.length, + }, + hits, + }, + }; +}; + +describe('get_logstash_info', () => { + it('return mapped data for result with hits, availability = true', () => { + const hits = [createResponseObjHit()]; + const res = createResponseFromHits(hits); + const result = handleResponse(res); + expect(result).toEqual({ + host: 'myhost', + uuid: 'd63b22f8-7f77-4a23-9aac-9813c760e0e0', + version: '8.0.0', + status: 'green', + uptime: undefined, + name: 'desktop-192-168-162-170.local', + pipeline: { + batch_size: 2, + workers: 2, + }, + http_address: '127.0.0.1:9600', + availability: true, + events: { + filtered: 300, + in: 300, + out: 300, + }, + reloads: { + successes: 5, + failures: 2, + }, + queue_type: 'persisted', + }); + }); + + it('return mapped data for result with hits, availability = false', () => { + const hits = [ + createResponseObjHit([ + { + path: '_source.logstash_stats.timestamp', + value: moment().subtract(11, 'minutes').format(), + }, + ]), + ]; + const res = createResponseFromHits(hits); + + const result = handleResponse(res); + expect(result).toEqual({ + host: 'myhost', + pipeline: { + batch_size: 2, + workers: 2, + }, + uuid: 'd63b22f8-7f77-4a23-9aac-9813c760e0e0', + version: '8.0.0', + status: 'green', + name: 'desktop-192-168-162-170.local', + http_address: '127.0.0.1:9600', + availability: false, + events: { + filtered: 300, + in: 300, + out: 300, + }, + reloads: { + successes: 5, + failures: 2, + }, + queue_type: 'persisted', + }); + }); + + it('default to no queue type if none specified', () => { + const hits = [ + createResponseObjHit([ + { + path: '_source.logstash_stats.queue', // delete queue property + }, + { + path: '_source.logstash_stats.timestamp', // update the timestamp property + value: moment().subtract(11, 'minutes').format(), + }, + ]), + ]; + const res = createResponseFromHits(hits); + const result = handleResponse(res); + expect(result).toEqual({ + host: 'myhost', + pipeline: { + batch_size: 2, + workers: 2, + }, + uuid: 'd63b22f8-7f77-4a23-9aac-9813c760e0e0', + version: '8.0.0', + status: 'green', + name: 'desktop-192-168-162-170.local', + http_address: '127.0.0.1:9600', + availability: false, + events: { + filtered: 300, + in: 300, + out: 300, + }, + reloads: { + successes: 5, + failures: 2, + }, + }); + }); + + it('works with standalone cluster', async () => { + const callWithRequest = jest.fn().mockReturnValue({ + then: jest.fn(), + }); + const req = ({ + server: { + plugins: { + elasticsearch: { + getCluster: () => ({ + callWithRequest, + }), + }, + }, + }, + } as unknown) as LegacyRequest; + await getNodeInfo(req, '.monitoring-logstash-*', { + clusterUuid: STANDALONE_CLUSTER_CLUSTER_UUID, + logstashUuid: 'logstash_uuid', + }); + expect(callWithRequest.mock.calls.length).toBe(1); + expect(callWithRequest.mock.calls[0].length).toBe(3); + expect(callWithRequest.mock.calls[0][2].body.query.bool.filter[0]).toBe( + standaloneClusterFilter + ); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts index 276b8b119bba3..ebd1128dce364 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts @@ -6,14 +6,11 @@ */ import { merge } from 'lodash'; -// @ts-ignore import { checkParam, MissingRequiredError } from '../error_missing_required'; -// @ts-ignore import { calculateAvailability } from '../calculate_availability'; import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; -// @ts-ignore import { standaloneClusterFilter } from '../standalone_clusters/standalone_cluster_query_filter'; export function handleResponse(resp: ElasticsearchResponse) { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts index 42d1b69aee5f3..153c2ece13830 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts @@ -6,13 +6,9 @@ */ import moment from 'moment'; -// @ts-ignore import { checkParam } from '../error_missing_required'; -// @ts-ignore import { createQuery } from '../create_query'; -// @ts-ignore import { calculateAvailability } from '../calculate_availability'; -// @ts-ignore import { LogstashMetric } from '../metrics'; import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts similarity index 51% rename from x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts index a4645edda73d0..ee41e12ea322b 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.ts @@ -11,6 +11,16 @@ import { getLogstashPipelineIds } from './get_pipeline_ids'; import { sortPipelines } from './sort_pipelines'; import { paginate } from '../pagination/paginate'; import { getMetrics } from '../details/get_metrics'; +import { + LegacyRequest, + Pipeline, + PipelineMetricKey, + PipelineMetricsRes, + PipelineNodeCountMetricKey, + PipelinesResponse, + PipelineThroughputMetricKey, + PipelineWithMetrics, +} from '../../types'; /** * This function performs an optimization around the pipeline listing tables in the UI. To avoid @@ -22,80 +32,109 @@ import { getMetrics } from '../details/get_metrics'; * * @param {*} req - Server request object * @param {*} lsIndexPattern - The index pattern to search against (`.monitoring-logstash-*`) - * @param {*} uuids - The optional `clusterUuid` and `logstashUuid` to filter the results from - * @param {*} metricSet - The array of metrics that are sortable in the UI + * @param {*} clusterUuid - clusterUuid to filter the results from + * @param {*} logstashUuid - logstashUuid to filter the results from + * @param {*} metrics - The array of metrics that are sortable in the UI * @param {*} pagination - ({ index, size }) * @param {*} sort - ({ field, direction }) * @param {*} queryText - Text that will be used to filter out pipelines */ -export async function getPaginatedPipelines( + +interface GetPaginatedPipelinesParams { + req: LegacyRequest; + lsIndexPattern: string; + clusterUuid: string; + logstashUuid?: string; + metrics: { + throughputMetric: PipelineThroughputMetricKey; + nodesCountMetric: PipelineNodeCountMetricKey; + }; + pagination: { index: number; size: number }; + sort: { field: PipelineMetricKey | ''; direction: 'asc' | 'desc' }; + queryText: string; +} +export async function getPaginatedPipelines({ req, lsIndexPattern, - { clusterUuid, logstashUuid }, - { throughputMetric, nodesCountMetric }, + clusterUuid, + logstashUuid, + metrics, pagination, - sort = { field: null }, - queryText -) { + sort = { field: '', direction: 'desc' }, + queryText, +}: GetPaginatedPipelinesParams) { + const { throughputMetric, nodesCountMetric } = metrics; const sortField = sort.field; const config = req.server.config(); - const size = config.get('monitoring.ui.max_bucket_size'); - const pipelines = await getLogstashPipelineIds( + // TODO type config + const size = (config.get('monitoring.ui.max_bucket_size') as unknown) as number; + let pipelines = await getLogstashPipelineIds({ req, lsIndexPattern, - { clusterUuid, logstashUuid }, - size - ); - + clusterUuid, + logstashUuid, + size, + }); + // this is needed for sorting if (sortField === throughputMetric) { - await getPaginatedThroughputData(pipelines, req, lsIndexPattern, throughputMetric); + pipelines = await getPaginatedThroughputData(pipelines, req, lsIndexPattern, throughputMetric); } else if (sortField === nodesCountMetric) { - await getPaginatedNodesData(pipelines, req, lsIndexPattern, nodesCountMetric); + pipelines = await getPaginatedNodesData(pipelines, req, lsIndexPattern, nodesCountMetric); } - // Filtering const filteredPipelines = filter(pipelines, queryText, ['id']); // We only support filtering by id right now - // Sorting const sortedPipelines = sortPipelines(filteredPipelines, sort); - // Pagination const pageOfPipelines = paginate(pagination, sortedPipelines); const response = { - pipelines: await getPipelines( + pipelines: await getPipelines({ req, lsIndexPattern, - pageOfPipelines, + pipelines: pageOfPipelines, throughputMetric, - nodesCountMetric - ), + nodesCountMetric, + }), totalPipelineCount: filteredPipelines.length, }; return processPipelinesAPIResponse(response, throughputMetric, nodesCountMetric); } -function processPipelinesAPIResponse(response, throughputMetricKey, nodesCountMetricKey) { - // Clone to avoid mutating original response - const processedResponse = cloneDeep(response); - +function processPipelinesAPIResponse( + response: { pipelines: PipelineWithMetrics[]; totalPipelineCount: number }, + throughputMetricKey: PipelineThroughputMetricKey, + nodeCountMetricKey: PipelineNodeCountMetricKey +) { // Normalize metric names for shared component code // Calculate latest throughput and node count for each pipeline - processedResponse.pipelines.forEach((pipeline) => { - pipeline.metrics = { - throughput: pipeline.metrics[throughputMetricKey], - nodesCount: pipeline.metrics[nodesCountMetricKey], - }; + const processedResponse = response.pipelines.reduce( + (acc, pipeline) => { + acc.pipelines.push({ + ...pipeline, + metrics: { + throughput: pipeline.metrics[throughputMetricKey], + nodesCount: pipeline.metrics[nodeCountMetricKey], + }, + latestThroughput: (last(pipeline.metrics[throughputMetricKey]?.data) || [])[1], + latestNodesCount: (last(pipeline.metrics[nodeCountMetricKey]?.data) || [])[1], + }); + return acc; + }, + { totalPipelineCount: response.totalPipelineCount, pipelines: [] } + ); - pipeline.latestThroughput = (last(pipeline.metrics.throughput.data) || [])[1]; - pipeline.latestNodesCount = (last(pipeline.metrics.nodesCount.data) || [])[1]; - }); return processedResponse; } -async function getPaginatedThroughputData(pipelines, req, lsIndexPattern, throughputMetric) { - const metricSeriesData = Object.values( +async function getPaginatedThroughputData( + pipelines: Pipeline[], + req: LegacyRequest, + lsIndexPattern: string, + throughputMetric: PipelineThroughputMetricKey +): Promise { + const metricSeriesData: any = Object.values( await Promise.all( pipelines.map((pipeline) => { return new Promise(async (resolve, reject) => { @@ -135,21 +174,33 @@ async function getPaginatedThroughputData(pipelines, req, lsIndexPattern, throug }) ) ); - - for (const pipelineAggregationData of metricSeriesData) { - for (const pipeline of pipelines) { - if (pipelineAggregationData.id === pipeline.id) { - const dataSeries = get(pipelineAggregationData, `metrics.${throughputMetric}.data`, [[]]); - if (dataSeries.length === 0) { - continue; - } - pipeline[throughputMetric] = dataSeries.pop()[1]; + return pipelines.reduce((acc, pipeline) => { + const match = metricSeriesData.find((metric: { id: string }) => metric.id === pipeline.id); + if (match) { + const dataSeries = get(match, `metrics.${throughputMetric}.data`, [[]]); + if (dataSeries.length) { + const newPipeline = { + ...pipeline, + [throughputMetric]: dataSeries.pop()[1], + }; + acc.push(newPipeline); + } else { + acc.push(pipeline); } + } else { + acc.push(pipeline); } - } + return acc; + }, []); } -async function getPaginatedNodesData(pipelines, req, lsIndexPattern, nodesCountMetric) { +async function getPaginatedNodesData( + pipelines: Pipeline[], + req: LegacyRequest, + lsIndexPattern: string, + nodesCountMetric: PipelineNodeCountMetricKey +): Promise { + const pipelineWithMetrics = cloneDeep(pipelines); const metricSeriesData = await getMetrics( req, lsIndexPattern, @@ -161,39 +212,71 @@ async function getPaginatedNodesData(pipelines, req, lsIndexPattern, nodesCountM }, }, ], - { pageOfPipelines: pipelines }, + { pageOfPipelines: pipelineWithMetrics }, 2 ); const { data } = metricSeriesData[nodesCountMetric][0] || [[]]; const pipelinesMap = (data.pop() || [])[1] || {}; if (!Object.keys(pipelinesMap).length) { - return; + return pipelineWithMetrics; } - pipelines.forEach((pipeline) => void (pipeline[nodesCountMetric] = pipelinesMap[pipeline.id])); + return pipelineWithMetrics.map((pipeline) => ({ + ...pipeline, + [nodesCountMetric]: pipelinesMap[pipeline.id], + })); } -async function getPipelines(req, lsIndexPattern, pipelines, throughputMetric, nodesCountMetric) { +async function getPipelines({ + req, + lsIndexPattern, + pipelines, + throughputMetric, + nodesCountMetric, +}: { + req: LegacyRequest; + lsIndexPattern: string; + pipelines: Pipeline[]; + throughputMetric: PipelineThroughputMetricKey; + nodesCountMetric: PipelineNodeCountMetricKey; +}): Promise { const throughputPipelines = await getThroughputPipelines( req, lsIndexPattern, pipelines, throughputMetric ); - const nodePipelines = await getNodePipelines(req, lsIndexPattern, pipelines, nodesCountMetric); + const nodeCountPipelines = await getNodePipelines( + req, + lsIndexPattern, + pipelines, + nodesCountMetric + ); const finalPipelines = pipelines.map(({ id }) => { - const pipeline = { + const matchThroughputPipeline = throughputPipelines.find((p) => p.id === id); + const matchNodesCountPipeline = nodeCountPipelines.find((p) => p.id === id); + return { id, metrics: { - [throughputMetric]: throughputPipelines.find((p) => p.id === id).metrics[throughputMetric], - [nodesCountMetric]: nodePipelines.find((p) => p.id === id).metrics[nodesCountMetric], + [throughputMetric]: + matchThroughputPipeline && throughputMetric in matchThroughputPipeline.metrics + ? matchThroughputPipeline.metrics[throughputMetric] + : undefined, + [nodesCountMetric]: + matchNodesCountPipeline && nodesCountMetric in matchNodesCountPipeline.metrics + ? matchNodesCountPipeline.metrics[nodesCountMetric] + : undefined, }, }; - return pipeline; }); return finalPipelines; } -async function getThroughputPipelines(req, lsIndexPattern, pipelines, throughputMetric) { +async function getThroughputPipelines( + req: LegacyRequest, + lsIndexPattern: string, + pipelines: Pipeline[], + throughputMetric: string +): Promise { const metricsResponse = await Promise.all( pipelines.map((pipeline) => { return new Promise(async (resolve, reject) => { @@ -231,11 +314,15 @@ async function getThroughputPipelines(req, lsIndexPattern, pipelines, throughput }); }) ); - - return Object.values(metricsResponse); + return Object.values(metricsResponse) as PipelineWithMetrics[]; } -async function getNodePipelines(req, lsIndexPattern, pipelines, nodesCountMetric) { +async function getNodePipelines( + req: LegacyRequest, + lsIndexPattern: string, + pipelines: Pipeline[], + nodesCountMetric: string +): Promise { const metricData = await getMetrics( req, lsIndexPattern, @@ -252,7 +339,7 @@ async function getNodePipelines(req, lsIndexPattern, pipelines, nodesCountMetric } ); - const metricObject = metricData[nodesCountMetric][0]; + const metricObject = metricData[nodesCountMetric][0] as PipelineMetricsRes; const pipelinesData = pipelines.map(({ id }) => { return { id, @@ -268,10 +355,10 @@ async function getNodePipelines(req, lsIndexPattern, pipelines, nodesCountMetric return pipelinesData; } -function reduceData({ id }, data) { +function reduceData(pipeline: Pipeline, data: any) { return { - id, - metrics: Object.keys(data).reduce((accum, metricName) => { + id: pipeline.id, + metrics: Object.keys(data).reduce((accum, metricName) => { accum[metricName] = data[metricName][0]; return accum; }, {}), diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.ts similarity index 94% rename from x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.ts index cb329db9a3855..d71c67dba524c 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.ts @@ -5,17 +5,19 @@ * 2.0. */ +import { ElasticsearchSourceLogstashPipelineVertex } from '../../../common/types/es'; import { _vertexStats, _enrichStateWithStatsAggregation } from './get_pipeline'; describe('get_pipeline', () => { describe('_vertexStats function', () => { - let vertex; - let vertexStatsBucket; - let totalProcessorsDurationInMillis; - let timeseriesIntervalInSeconds; + let vertex: ElasticsearchSourceLogstashPipelineVertex; + let vertexStatsBucket: any; + let totalProcessorsDurationInMillis: number; + let timeseriesIntervalInSeconds: number; beforeEach(() => { vertex = { + id: 'test', plugin_type: 'input', }; @@ -47,6 +49,7 @@ describe('get_pipeline', () => { describe('vertex represents filter plugin', () => { beforeEach(() => { vertex = { + id: 'test', plugin_type: 'filter', }; }); @@ -70,6 +73,7 @@ describe('get_pipeline', () => { describe('vertex represents output plugin', () => { beforeEach(() => { vertex = { + id: 'test', plugin_type: 'output', }; }); @@ -92,9 +96,9 @@ describe('get_pipeline', () => { }); describe('_enrichStateWithStatsAggregation function', () => { - let stateDocument; - let statsAggregation; - let timeseriesInterval; + let stateDocument: any; + let statsAggregation: object; + let timeseriesInterval: number; beforeEach(() => { stateDocument = { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts index d8bfd91a4aec8..7882256f83d22 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts @@ -7,14 +7,11 @@ import boom from '@hapi/boom'; import { get } from 'lodash'; -// @ts-ignore import { checkParam } from '../error_missing_required'; import { getPipelineStateDocument } from './get_pipeline_state_document'; -// @ts-ignore import { getPipelineStatsAggregation } from './get_pipeline_stats_aggregation'; -// @ts-ignore import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; -import { LegacyRequest } from '../../types'; +import { LegacyRequest, PipelineVersion } from '../../types'; import { ElasticsearchSource, ElasticsearchSourceLogstashPipelineVertex, @@ -119,16 +116,10 @@ export async function getPipeline( lsIndexPattern: string, clusterUuid: string, pipelineId: string, - version: { firstSeen: string; lastSeen: string; hash: string } + version: PipelineVersion ) { checkParam(lsIndexPattern, 'lsIndexPattern in getPipeline'); - const options: any = { - clusterUuid, - pipelineId, - version, - }; - // Determine metrics' timeseries interval based on version's timespan const minIntervalSeconds = config.get('monitoring.ui.min_interval_seconds'); const timeseriesInterval = calculateTimeseriesInterval( @@ -138,8 +129,21 @@ export async function getPipeline( ); const [stateDocument, statsAggregation] = await Promise.all([ - getPipelineStateDocument(req, lsIndexPattern, options), - getPipelineStatsAggregation(req, lsIndexPattern, timeseriesInterval, options), + getPipelineStateDocument({ + req, + logstashIndexPattern: lsIndexPattern, + clusterUuid, + pipelineId, + version, + }), + getPipelineStatsAggregation({ + req, + logstashIndexPattern: lsIndexPattern, + timeseriesInterval, + clusterUuid, + pipelineId, + version, + }), ]); if (stateDocument === null || !statsAggregation) { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts index 1a5595d45ffbb..c9b7a3adfc18e 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts @@ -7,16 +7,24 @@ import moment from 'moment'; import { get } from 'lodash'; -import { LegacyRequest, Bucket } from '../../types'; +import { LegacyRequest, Bucket, Pipeline } from '../../types'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; -export async function getLogstashPipelineIds( - req: LegacyRequest, - logstashIndexPattern: string, - { clusterUuid, logstashUuid }: { clusterUuid: string; logstashUuid?: string }, - size: number -) { +interface GetLogstashPipelineIdsParams { + req: LegacyRequest; + lsIndexPattern: string; + clusterUuid: string; + size: number; + logstashUuid?: string; +} +export async function getLogstashPipelineIds({ + req, + lsIndexPattern, + clusterUuid, + logstashUuid, + size, +}: GetLogstashPipelineIdsParams): Promise { const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); @@ -26,7 +34,7 @@ export async function getLogstashPipelineIds( } const params = { - index: logstashIndexPattern, + index: lsIndexPattern, size: 0, ignore_unavailable: true, filter_path: ['aggregations.nest.id.buckets', 'aggregations.nest_mb.id.buckets'], diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts index 61c99c3a069b3..8558e117fb2f8 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts @@ -5,22 +5,24 @@ * 2.0. */ -// @ts-ignore import { createQuery } from '../create_query'; -// @ts-ignore import { LogstashMetric } from '../metrics'; -import { LegacyRequest } from '../../types'; +import { LegacyRequest, PipelineVersion } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; -export async function getPipelineStateDocument( - req: LegacyRequest, - logstashIndexPattern: string, - { - clusterUuid, - pipelineId, - version, - }: { clusterUuid: string; pipelineId: string; version: { hash: string } } -) { +export async function getPipelineStateDocument({ + req, + logstashIndexPattern, + clusterUuid, + pipelineId, + version, +}: { + req: LegacyRequest; + logstashIndexPattern: string; + clusterUuid: string; + pipelineId: string; + version: PipelineVersion; +}) { const { callWithRequest } = req.server.plugins?.elasticsearch.getCluster('monitoring'); const filters = [ { term: { 'logstash_state.pipeline.id': pipelineId } }, @@ -52,7 +54,6 @@ export async function getPipelineStateDocument( }; const resp = (await callWithRequest(req, 'search', params)) as ElasticsearchResponse; - // Return null if doc not found return resp.hits?.hits[0]?._source ?? null; } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts similarity index 80% rename from x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts index 4d9d2a720a162..0205ce21c59b9 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.ts @@ -5,16 +5,22 @@ * 2.0. */ +import { LegacyRequest, PipelineVersion } from '../../types'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; -function scalarCounterAggregation(field, fieldPath, ephemeralIdField, maxBucketSize) { +function scalarCounterAggregation( + field: string, + fieldPath: string, + ephemeralIdField: string, + maxBucketSize: string +) { const fullPath = `${fieldPath}.${field}`; const byEphemeralIdName = `${field}_temp_by_ephemeral_id`; const sumName = `${field}_total`; - const aggs = {}; + const aggs: { [key: string]: any } = {}; aggs[byEphemeralIdName] = { terms: { @@ -46,7 +52,7 @@ function scalarCounterAggregation(field, fieldPath, ephemeralIdField, maxBucketS return aggs; } -function nestedVertices(maxBucketSize) { +function nestedVertices(maxBucketSize: string) { const fieldPath = 'logstash_stats.pipelines.vertices'; const ephemeralIdField = 'logstash_stats.pipelines.vertices.pipeline_ephemeral_id'; @@ -73,7 +79,7 @@ function nestedVertices(maxBucketSize) { }; } -function createScopedAgg(pipelineId, pipelineHash, agg) { +function createScopedAgg(pipelineId: string, pipelineHash: string, agg: { [key: string]: any }) { return { pipelines: { nested: { path: 'logstash_stats.pipelines' }, @@ -95,13 +101,13 @@ function createScopedAgg(pipelineId, pipelineHash, agg) { } function fetchPipelineLatestStats( - query, - logstashIndexPattern, - pipelineId, - version, - maxBucketSize, - callWithRequest, - req + query: object, + logstashIndexPattern: string, + pipelineId: string, + version: PipelineVersion, + maxBucketSize: string, + callWithRequest: any, + req: LegacyRequest ) { const params = { index: logstashIndexPattern, @@ -115,7 +121,7 @@ function fetchPipelineLatestStats( 'aggregations.pipelines.scoped.total_processor_duration_stats', ], body: { - query: query, + query, aggs: createScopedAgg(pipelineId, version.hash, { vertices: nestedVertices(maxBucketSize), total_processor_duration_stats: { @@ -130,12 +136,21 @@ function fetchPipelineLatestStats( return callWithRequest(req, 'search', params); } -export function getPipelineStatsAggregation( +export function getPipelineStatsAggregation({ req, logstashIndexPattern, timeseriesInterval, - { clusterUuid, start, end, pipelineId, version } -) { + clusterUuid, + pipelineId, + version, +}: { + req: LegacyRequest; + logstashIndexPattern: string; + timeseriesInterval: number; + clusterUuid: string; + pipelineId: string; + version: PipelineVersion; +}) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const filters = [ { @@ -153,8 +168,8 @@ export function getPipelineStatsAggregation( }, ]; - start = version.lastSeen - timeseriesInterval * 1000; - end = version.lastSeen; + const start = version.lastSeen - timeseriesInterval * 1000; + const end = version.lastSeen; const query = createQuery({ types: ['stats', 'logstash_stats'], @@ -172,6 +187,8 @@ export function getPipelineStatsAggregation( logstashIndexPattern, pipelineId, version, + // @ts-ignore not undefined, need to get correct config + // https://github.com/elastic/kibana/issues/112146 config.get('monitoring.ui.max_bucket_size'), callWithRequest, req diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts similarity index 78% rename from x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts index c52d41a363055..18330a83185ca 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.ts @@ -5,14 +5,25 @@ * 2.0. */ +import { get } from 'lodash'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; -import { get } from 'lodash'; import { checkParam } from '../error_missing_required'; +import { LegacyRequest } from '../../types'; -function fetchPipelineVersions(...args) { - const [req, config, logstashIndexPattern, clusterUuid, pipelineId] = args; - checkParam(logstashIndexPattern, 'logstashIndexPattern in getPipelineVersions'); +function fetchPipelineVersions({ + req, + lsIndexPattern, + clusterUuid, + pipelineId, +}: { + req: LegacyRequest; + lsIndexPattern: string; + clusterUuid: string; + pipelineId: string; +}) { + const config = req.server.config(); + checkParam(lsIndexPattern, 'logstashIndexPattern in getPipelineVersions'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const filters = [ @@ -80,7 +91,7 @@ function fetchPipelineVersions(...args) { }; const params = { - index: logstashIndexPattern, + index: lsIndexPattern, size: 0, ignore_unavailable: true, body: { @@ -93,20 +104,25 @@ function fetchPipelineVersions(...args) { return callWithRequest(req, 'search', params); } -export function _handleResponse(response) { +export function _handleResponse(response: any) { const pipelineHashes = get( response, 'aggregations.pipelines.scoped.by_pipeline_hash.buckets', [] ); - return pipelineHashes.map((pipelineHash) => ({ + return pipelineHashes.map((pipelineHash: any) => ({ hash: pipelineHash.key, firstSeen: get(pipelineHash, 'path_to_root.first_seen.value'), lastSeen: get(pipelineHash, 'path_to_root.last_seen.value'), })); } -export async function getPipelineVersions(...args) { - const response = await fetchPipelineVersions(...args); +export async function getPipelineVersions(args: { + req: LegacyRequest; + lsIndexPattern: string; + clusterUuid: string; + pipelineId: string; +}) { + const response = await fetchPipelineVersions(args); return _handleResponse(response); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts index e41eea0bce64a..b75bf13dafd62 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts @@ -7,14 +7,11 @@ import boom from '@hapi/boom'; import { get } from 'lodash'; -// @ts-ignore import { checkParam } from '../error_missing_required'; import { getPipelineStateDocument } from './get_pipeline_state_document'; -// @ts-ignore import { getPipelineVertexStatsAggregation } from './get_pipeline_vertex_stats_aggregation'; -// @ts-ignore import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; -import { LegacyRequest } from '../../types'; +import { LegacyRequest, PipelineVersion } from '../../types'; import { ElasticsearchSource, ElasticsearchSourceLogstashPipelineVertex, @@ -135,18 +132,11 @@ export async function getPipelineVertex( lsIndexPattern: string, clusterUuid: string, pipelineId: string, - version: { hash: string; firstSeen: string; lastSeen: string }, + version: PipelineVersion, vertexId: string ) { checkParam(lsIndexPattern, 'lsIndexPattern in getPipeline'); - const options = { - clusterUuid, - pipelineId, - version, - vertexId, - }; - // Determine metrics' timeseries interval based on version's timespan const minIntervalSeconds = config.get('monitoring.ui.min_interval_seconds'); const timeseriesInterval = calculateTimeseriesInterval( @@ -156,8 +146,22 @@ export async function getPipelineVertex( ); const [stateDocument, statsAggregation] = await Promise.all([ - getPipelineStateDocument(req, lsIndexPattern, options), - getPipelineVertexStatsAggregation(req, lsIndexPattern, timeseriesInterval, options), + getPipelineStateDocument({ + req, + logstashIndexPattern: lsIndexPattern, + clusterUuid, + pipelineId, + version, + }), + getPipelineVertexStatsAggregation({ + req, + logstashIndexPattern: lsIndexPattern, + timeSeriesIntervalInSeconds: timeseriesInterval, + clusterUuid, + pipelineId, + version, + vertexId, + }), ]); if (stateDocument === null || !statsAggregation) { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts index 97a8c463a2259..875d6ef962981 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.ts @@ -5,16 +5,22 @@ * 2.0. */ +import { LegacyRequest, PipelineVersion } from '../../types'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; -function scalarCounterAggregation(field, fieldPath, ephemeralIdField, maxBucketSize) { +function scalarCounterAggregation( + field: string, + fieldPath: string, + ephemeralIdField: string, + maxBucketSize: number +) { const fullPath = `${fieldPath}.${field}`; const byEphemeralIdName = `${field}_temp_by_ephemeral_id`; const sumName = `${field}_total`; - const aggs = {}; + const aggs: any = {}; aggs[byEphemeralIdName] = { terms: { @@ -46,11 +52,11 @@ function scalarCounterAggregation(field, fieldPath, ephemeralIdField, maxBucketS return aggs; } -function createAggsObjectFromAggsList(aggsList) { - return aggsList.reduce((aggsSoFar, agg) => ({ ...aggsSoFar, ...agg }), {}); +function createAggsObjectFromAggsList(aggsList: any) { + return aggsList.reduce((aggsSoFar: object, agg: object) => ({ ...aggsSoFar, ...agg }), {}); } -function createNestedVertexAgg(vertexId, maxBucketSize) { +function createNestedVertexAgg(vertexId: string, maxBucketSize: number) { const fieldPath = 'logstash_stats.pipelines.vertices'; const ephemeralIdField = 'logstash_stats.pipelines.vertices.pipeline_ephemeral_id'; @@ -96,7 +102,7 @@ function createTotalProcessorDurationStatsAgg() { }; } -function createScopedAgg(pipelineId, pipelineHash, ...aggsList) { +function createScopedAgg(pipelineId: string, pipelineHash: string, ...aggsList: object[]) { return { pipelines: { nested: { path: 'logstash_stats.pipelines' }, @@ -117,7 +123,7 @@ function createScopedAgg(pipelineId, pipelineHash, ...aggsList) { }; } -function createTimeSeriesAgg(timeSeriesIntervalInSeconds, ...aggsList) { +function createTimeSeriesAgg(timeSeriesIntervalInSeconds: number, ...aggsList: object[]) { return { timeseries: { date_histogram: { @@ -129,7 +135,7 @@ function createTimeSeriesAgg(timeSeriesIntervalInSeconds, ...aggsList) { }; } -function fetchPipelineVertexTimeSeriesStats( +function fetchPipelineVertexTimeSeriesStats({ query, logstashIndexPattern, pipelineId, @@ -138,8 +144,18 @@ function fetchPipelineVertexTimeSeriesStats( timeSeriesIntervalInSeconds, maxBucketSize, callWithRequest, - req -) { + req, +}: { + query: object; + logstashIndexPattern: string; + pipelineId: string; + version: PipelineVersion; + vertexId: string; + timeSeriesIntervalInSeconds: number; + maxBucketSize: number; + callWithRequest: (req: any, endpoint: string, params: any) => Promise; + req: LegacyRequest; +}) { const aggs = { ...createTimeSeriesAgg( timeSeriesIntervalInSeconds, @@ -165,7 +181,7 @@ function fetchPipelineVertexTimeSeriesStats( 'aggregations.timeseries.buckets.pipelines.scoped.total_processor_duration_stats', ], body: { - query: query, + query, aggs, }, }; @@ -173,12 +189,23 @@ function fetchPipelineVertexTimeSeriesStats( return callWithRequest(req, 'search', params); } -export function getPipelineVertexStatsAggregation( +export function getPipelineVertexStatsAggregation({ req, logstashIndexPattern, timeSeriesIntervalInSeconds, - { clusterUuid, start, end, pipelineId, version, vertexId } -) { + clusterUuid, + pipelineId, + version, + vertexId, +}: { + req: LegacyRequest; + logstashIndexPattern: string; + timeSeriesIntervalInSeconds: number; + clusterUuid: string; + pipelineId: string; + version: PipelineVersion; + vertexId: string; +}) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const filters = [ { @@ -196,8 +223,8 @@ export function getPipelineVertexStatsAggregation( }, ]; - start = version.firstSeen; - end = version.lastSeen; + const start = version.firstSeen; + const end = version.lastSeen; const query = createQuery({ types: ['stats', 'logstash_stats'], @@ -210,15 +237,17 @@ export function getPipelineVertexStatsAggregation( const config = req.server.config(); - return fetchPipelineVertexTimeSeriesStats( + return fetchPipelineVertexTimeSeriesStats({ query, logstashIndexPattern, pipelineId, version, vertexId, timeSeriesIntervalInSeconds, - config.get('monitoring.ui.max_bucket_size'), + // @ts-ignore not undefined, need to get correct config + // https://github.com/elastic/kibana/issues/112146 + maxBucketSize: config.get('monitoring.ui.max_bucket_size'), callWithRequest, - req - ); + req, + }); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js b/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.ts similarity index 56% rename from x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js rename to x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.ts index 19e0a78865635..5ecd4f6632c48 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/sort_pipelines.ts @@ -6,11 +6,15 @@ */ import { orderBy } from 'lodash'; +import { Pipeline, PipelineMetricKey } from '../../types'; -export function sortPipelines(pipelines, sort) { +export function sortPipelines( + pipelines: Pipeline[], + sort: { field: PipelineMetricKey | ''; direction: 'asc' | 'desc' } +): Pipeline[] { if (!sort) { return pipelines; } - return orderBy(pipelines, (pipeline) => pipeline[sort.field], sort.direction); + return orderBy(pipelines, [sort.field], [sort.direction]); } diff --git a/x-pack/plugins/monitoring/server/lib/pagination/filter.js b/x-pack/plugins/monitoring/server/lib/pagination/filter.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/pagination/filter.js rename to x-pack/plugins/monitoring/server/lib/pagination/filter.ts index 96c4fda34eb84..d6d5f55dcdd3c 100644 --- a/x-pack/plugins/monitoring/server/lib/pagination/filter.js +++ b/x-pack/plugins/monitoring/server/lib/pagination/filter.ts @@ -7,14 +7,19 @@ import { get } from 'lodash'; -function defaultFilterFn(value, query) { +function defaultFilterFn(value: string, query: string) { if (value.toLowerCase().includes(query.toLowerCase())) { return true; } return false; } -export function filter(data, queryText, fields, filterFn = defaultFilterFn) { +export function filter( + data: T[], + queryText: string, + fields: string[], + filterFn = defaultFilterFn +): T[] { return data.filter((item) => { for (const field of fields) { if (filterFn(get(item, field, ''), queryText)) { diff --git a/x-pack/plugins/monitoring/server/lib/pagination/paginate.js b/x-pack/plugins/monitoring/server/lib/pagination/paginate.ts similarity index 77% rename from x-pack/plugins/monitoring/server/lib/pagination/paginate.js rename to x-pack/plugins/monitoring/server/lib/pagination/paginate.ts index 2d0e9e83ea901..15d3cb085787e 100644 --- a/x-pack/plugins/monitoring/server/lib/pagination/paginate.js +++ b/x-pack/plugins/monitoring/server/lib/pagination/paginate.ts @@ -5,7 +5,7 @@ * 2.0. */ -export function paginate({ size, index }, data) { +export function paginate({ size, index }: { size: number; index: number }, data: T[]): T[] { const start = index * size; return data.slice(start, start + size); } diff --git a/x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.js b/x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.js rename to x-pack/plugins/monitoring/server/lib/standalone_clusters/standalone_cluster_query_filter.ts diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js index 8b8e5cdcccdb4..b68a708b1f208 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js @@ -61,7 +61,13 @@ export function logstashPipelineRoute(server) { // Figure out which version of the pipeline we want to show let versions; try { - versions = await getPipelineVersions(req, config, lsIndexPattern, clusterUuid, pipelineId); + versions = await getPipelineVersions({ + req, + config, + lsIndexPattern, + clusterUuid, + pipelineId, + }); } catch (err) { return handleError(err, req); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js index 5be8b9b965d95..c881ff7b3d23c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js @@ -40,7 +40,7 @@ export function logstashClusterPipelineIdsRoute(server) { const size = config.get('monitoring.ui.max_bucket_size'); try { - const pipelines = await getLogstashPipelineIds(req, lsIndexPattern, { clusterUuid }, size); + const pipelines = await getLogstashPipelineIds({ req, lsIndexPattern, clusterUuid, size }); return pipelines; } catch (err) { throw handleError(err, req); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js index 646cd047d8b41..1f7a5e1d436b1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js @@ -61,17 +61,16 @@ export function logstashClusterPipelinesRoute(server) { if (sort) { sort.field = sortMetricSetMap[sort.field] || sort.field; } - try { - const response = await getPaginatedPipelines( + const response = await getPaginatedPipelines({ req, lsIndexPattern, - { clusterUuid }, - { throughputMetric, nodesCountMetric }, + clusterUuid, + metrics: { throughputMetric, nodesCountMetric }, pagination, sort, - queryText - ); + queryText, + }); return { ...response, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js index c2af754af4563..47b8fd81a4d44 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js @@ -64,15 +64,16 @@ export function logstashNodePipelinesRoute(server) { } try { - const response = await getPaginatedPipelines( + const response = await getPaginatedPipelines({ req, lsIndexPattern, - { clusterUuid, logstashUuid }, - { throughputMetric, nodesCountMetric }, + clusterUuid, + logstashUuid, + metrics: { throughputMetric, nodesCountMetric }, pagination, sort, - queryText - ); + queryText, + }); return { ...response, diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index e1010a7a5fd98..425c7f239138a 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -183,3 +183,77 @@ export interface ClusterSettingsReasonResponse { } export type ErrorTypes = Error | Boom.Boom | ResponseError | ElasticsearchClientError; + +export type Pipeline = { + id: string; + nodeIds: string[]; +} & { + [key in PipelineMetricKey]?: number; +}; + +export type PipelineMetricKey = + | 'logstash_cluster_pipeline_throughput' + | 'logstash_cluster_pipeline_node_count' + | 'logstash_node_pipeline_node_count' + | 'logstash_node_pipeline_throughput'; + +export type PipelineThroughputMetricKey = + | 'logstash_cluster_pipeline_throughput' + | 'logstash_node_pipeline_throughput'; + +export type PipelineNodeCountMetricKey = + | 'logstash_cluster_pipeline_node_count' + | 'logstash_node_pipeline_node_count'; + +export interface PipelineWithMetrics { + id: string; + metrics: { + logstash_cluster_pipeline_throughput?: PipelineMetricsProcessed; + logstash_cluster_pipeline_node_count?: PipelineMetricsProcessed; + logstash_node_pipeline_throughput?: PipelineMetricsProcessed; + logstash_node_pipeline_node_count?: PipelineMetricsProcessed; + }; +} + +export interface PipelineResponse { + id: string; + latestThroughput: number | null; + latestNodesCount: number | null; + metrics: { + nodesCount?: PipelineMetricsProcessed; + throughput?: PipelineMetricsProcessed; + }; +} +export interface PipelinesResponse { + pipelines: PipelineResponse[]; + totalPipelineCount: number; +} +export interface PipelineMetrics { + bucket_size: string; + timeRange: { + min: number; + max: number; + }; + metric: { + app: string; + field: string; + label: string; + description: string; + units: string; + format: string; + hasCalculation: boolean; + isDerivative: boolean; + }; +} +export type PipelineMetricsRes = PipelineMetrics & { + data: Array<[number, { [key: string]: number }]>; +}; +export type PipelineMetricsProcessed = PipelineMetrics & { + data: Array>; +}; + +export interface PipelineVersion { + firstSeen: number; + lastSeen: number; + hash: string; +} From 378f2ed2f7430ac915f4609934de99c172b640b9 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Sep 2021 12:33:42 -0700 Subject: [PATCH 35/39] skip flaky suite (#112164) --- x-pack/test/functional/apps/discover/reporting.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 11a442d3500b5..32ed5af506cc3 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -22,7 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': setValue }); }; - describe('Discover CSV Export', () => { + // Failing: See https://github.com/elastic/kibana/issues/112164 + describe.skip('Discover CSV Export', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); From 2976f33618188c7898c7304f7166f5b2bd34a091 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 14 Sep 2021 13:20:53 -0700 Subject: [PATCH 36/39] [eslint] add rule to forbid async forEach bodies (#111637) Co-authored-by: spalger --- .../elastic-eslint-config-kibana/.eslintrc.js | 1 + packages/kbn-eslint-plugin-eslint/index.js | 1 + .../rules/no_async_foreach.js | 62 ++++++++++++++ .../rules/no_async_foreach.test.js | 72 ++++++++++++++++ packages/kbn-std/BUILD.bazel | 5 +- packages/kbn-std/src/index.ts | 8 ++ .../kbn-std/src/iteration/for_each.test.ts | 81 ++++++++++++++++++ packages/kbn-std/src/iteration/for_each.ts | 44 ++++++++++ packages/kbn-std/src/iteration/index.ts | 11 +++ packages/kbn-std/src/iteration/map.test.ts | 82 +++++++++++++++++++ packages/kbn-std/src/iteration/map.ts | 63 ++++++++++++++ .../kbn-std/src/iteration/observable.test.ts | 81 ++++++++++++++++++ packages/kbn-std/src/iteration/observable.ts | 49 +++++++++++ .../kbn-std/src/iteration/test_helpers.ts | 17 ++++ packages/kbn-std/src/iteration/types.ts | 13 +++ .../multiple_es_nodes.test.ts | 13 +-- .../multiple_kibana_nodes.test.ts | 13 +-- src/plugins/bfetch/server/plugin.ts | 22 ++--- .../vis_type_timelion/server/lib/reduce.js | 22 ++--- .../vislib/components/legend/legend.tsx | 4 +- x-pack/plugins/event_log/server/es/init.ts | 8 +- .../delete_exception_list_items_by_list.ts | 3 +- .../plugins/maps/public/index_pattern_util.ts | 10 +-- .../server/services/uninstall_transforms.ts | 3 +- .../public/context/has_data_context.tsx | 3 +- .../rollup/server/collectors/helpers.ts | 4 +- .../confirm_delete_users.tsx | 4 +- .../detection_engine/rules/delete_rules.ts | 5 +- x-pack/test/accessibility/apps/helpers.ts | 4 +- .../apis/uptime/rest/snapshot.ts | 4 +- .../apis/epm/data_stream.ts | 7 +- .../import_geojson/file_indexing_panel.js | 4 +- .../alert_create_flyout.ts | 3 +- 33 files changed, 652 insertions(+), 74 deletions(-) create mode 100644 packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.js create mode 100644 packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.test.js create mode 100644 packages/kbn-std/src/iteration/for_each.test.ts create mode 100644 packages/kbn-std/src/iteration/for_each.ts create mode 100644 packages/kbn-std/src/iteration/index.ts create mode 100644 packages/kbn-std/src/iteration/map.test.ts create mode 100644 packages/kbn-std/src/iteration/map.ts create mode 100644 packages/kbn-std/src/iteration/observable.test.ts create mode 100644 packages/kbn-std/src/iteration/observable.ts create mode 100644 packages/kbn-std/src/iteration/test_helpers.ts create mode 100644 packages/kbn-std/src/iteration/types.ts diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 38c0c43132564..1c6b96c81afc7 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -92,5 +92,6 @@ module.exports = { ], '@kbn/eslint/no_async_promise_body': 'error', + '@kbn/eslint/no_async_foreach': 'error', }, }; diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index cf96cd9e801ba..a37d3c762a748 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -14,5 +14,6 @@ module.exports = { module_migration: require('./rules/module_migration'), no_export_all: require('./rules/no_export_all'), no_async_promise_body: require('./rules/no_async_promise_body'), + no_async_foreach: require('./rules/no_async_foreach'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.js b/packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.js new file mode 100644 index 0000000000000..d76d6a61a659b --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.js @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const tsEstree = require('@typescript-eslint/typescript-estree'); +const esTypes = tsEstree.AST_NODE_TYPES; + +/** @typedef {import("eslint").Rule.RuleModule} Rule */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Node} Node */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.CallExpression} CallExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.FunctionExpression} FunctionExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ArrowFunctionExpression} ArrowFunctionExpression */ +/** @typedef {import("eslint").Rule.RuleFixer} Fixer */ + +const ERROR_MSG = + 'Passing an async function to .forEach() prevents promise rejections from being handled. Use asyncForEach() or similar helper from "@kbn/std" instead.'; + +/** + * @param {Node} node + * @returns {node is ArrowFunctionExpression | FunctionExpression} + */ +const isFunc = (node) => + node.type === esTypes.ArrowFunctionExpression || node.type === esTypes.FunctionExpression; + +/** + * @param {any} context + * @param {CallExpression} node + */ +const isAsyncForEachCall = (node) => { + return ( + node.callee.type === esTypes.MemberExpression && + node.callee.property.type === esTypes.Identifier && + node.callee.property.name === 'forEach' && + node.arguments.length >= 1 && + isFunc(node.arguments[0]) && + node.arguments[0].async + ); +}; + +/** @type {Rule} */ +module.exports = { + meta: { + fixable: 'code', + schema: [], + }, + create: (context) => ({ + CallExpression(_) { + const node = /** @type {CallExpression} */ (_); + + if (isAsyncForEachCall(node)) { + context.report({ + message: ERROR_MSG, + loc: node.arguments[0].loc, + }); + } + }, + }), +}; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.test.js new file mode 100644 index 0000000000000..19c26fa8cc77b --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_async_foreach.test.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { RuleTester } = require('eslint'); +const rule = require('./no_async_foreach'); +const dedent = require('dedent'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('@kbn/eslint/no_async_foreach', rule, { + valid: [ + { + code: dedent` + array.forEach((a) => { + b(a) + }) + `, + }, + { + code: dedent` + array.forEach(function (a) { + b(a) + }) + `, + }, + ], + + invalid: [ + { + code: dedent` + array.forEach(async (a) => { + await b(a) + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to .forEach() prevents promise rejections from being handled. Use asyncForEach() or similar helper from "@kbn/std" instead.', + }, + ], + }, + { + code: dedent` + array.forEach(async function (a) { + await b(a) + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to .forEach() prevents promise rejections from being handled. Use asyncForEach() or similar helper from "@kbn/std" instead.', + }, + ], + }, + ], +}); diff --git a/packages/kbn-std/BUILD.bazel b/packages/kbn-std/BUILD.bazel index 571d3c061c138..182722c642238 100644 --- a/packages/kbn-std/BUILD.bazel +++ b/packages/kbn-std/BUILD.bazel @@ -9,7 +9,10 @@ SOURCE_FILES = glob( [ "src/**/*.ts", ], - exclude = ["**/*.test.*"], + exclude = [ + "**/*.test.*", + "**/test_helpers.ts", + ], ) SRCS = SOURCE_FILES diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index d79594c97cec7..33b40c20039f2 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -18,3 +18,11 @@ export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; export { ensureNoUnsafeProperties } from './ensure_no_unsafe_properties'; export * from './rxjs_7'; +export { + map$, + mapWithLimit$, + asyncMap, + asyncMapWithLimit, + asyncForEach, + asyncForEachWithLimit, +} from './iteration'; diff --git a/packages/kbn-std/src/iteration/for_each.test.ts b/packages/kbn-std/src/iteration/for_each.test.ts new file mode 100644 index 0000000000000..a10c204ffa4ea --- /dev/null +++ b/packages/kbn-std/src/iteration/for_each.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as Rx from 'rxjs'; + +import { asyncForEach, asyncForEachWithLimit } from './for_each'; +import { list, sleep } from './test_helpers'; + +jest.mock('./observable'); +const mockMapWithLimit$: jest.Mock = jest.requireMock('./observable').mapWithLimit$; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('asyncForEachWithLimit', () => { + it('calls mapWithLimit$ and resolves with undefined when it completes', async () => { + const iter = list(10); + const limit = 5; + const fn = jest.fn(); + + const result$ = new Rx.Subject(); + mockMapWithLimit$.mockImplementation(() => result$); + const promise = asyncForEachWithLimit(iter, limit, fn); + + let resolved = false; + promise.then(() => (resolved = true)); + + expect(mockMapWithLimit$).toHaveBeenCalledTimes(1); + expect(mockMapWithLimit$).toHaveBeenCalledWith(iter, limit, fn); + + expect(resolved).toBe(false); + result$.next(1); + result$.next(2); + result$.next(3); + + await sleep(10); + expect(resolved).toBe(false); + + result$.complete(); + await expect(promise).resolves.toBe(undefined); + }); + + it('resolves when iterator is empty', async () => { + mockMapWithLimit$.mockImplementation((x) => Rx.from(x)); + await expect(asyncForEachWithLimit([], 100, async () => 'foo')).resolves.toBe(undefined); + }); +}); + +describe('asyncForEach', () => { + it('calls mapWithLimit$ without limit and resolves with undefined when it completes', async () => { + const iter = list(10); + const fn = jest.fn(); + + const result$ = new Rx.Subject(); + mockMapWithLimit$.mockImplementation(() => result$); + const promise = asyncForEach(iter, fn); + + let resolved = false; + promise.then(() => (resolved = true)); + + expect(mockMapWithLimit$).toHaveBeenCalledTimes(1); + expect(mockMapWithLimit$).toHaveBeenCalledWith(iter, Infinity, fn); + + expect(resolved).toBe(false); + result$.next(1); + result$.next(2); + result$.next(3); + + await sleep(10); + expect(resolved).toBe(false); + + result$.complete(); + await expect(promise).resolves.toBe(undefined); + }); +}); diff --git a/packages/kbn-std/src/iteration/for_each.ts b/packages/kbn-std/src/iteration/for_each.ts new file mode 100644 index 0000000000000..bd23d2e0e6c11 --- /dev/null +++ b/packages/kbn-std/src/iteration/for_each.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { defaultIfEmpty } from 'rxjs/operators'; + +import { lastValueFrom } from '../rxjs_7'; +import { mapWithLimit$ } from './observable'; +import { IterableInput, AsyncMapFn } from './types'; + +/** + * Creates a promise which resolves with `undefined` after calling `fn` for each + * item in `iterable`. `fn` can return either a Promise or Observable. If `fn` + * returns observables then they will properly abort if an error occurs. + * + * @param iterable Items to iterate + * @param fn Function to call for each item + */ +export async function asyncForEach(iterable: IterableInput, fn: AsyncMapFn) { + await lastValueFrom(mapWithLimit$(iterable, Infinity, fn).pipe(defaultIfEmpty())); +} + +/** + * Creates a promise which resolves with `undefined` after calling `fn` for each + * item in `iterable`. `fn` can return either a Promise or Observable. If `fn` + * returns observables then they will properly abort if an error occurs. + * + * The number of concurrent executions of `fn` is limited by `limit`. + * + * @param iterable Items to iterate + * @param limit Maximum number of operations to run in parallel + * @param fn Function to call for each item + */ +export async function asyncForEachWithLimit( + iterable: IterableInput, + limit: number, + fn: AsyncMapFn +) { + await lastValueFrom(mapWithLimit$(iterable, limit, fn).pipe(defaultIfEmpty())); +} diff --git a/packages/kbn-std/src/iteration/index.ts b/packages/kbn-std/src/iteration/index.ts new file mode 100644 index 0000000000000..e9ed7655270b0 --- /dev/null +++ b/packages/kbn-std/src/iteration/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './observable'; +export * from './for_each'; +export * from './map'; diff --git a/packages/kbn-std/src/iteration/map.test.ts b/packages/kbn-std/src/iteration/map.test.ts new file mode 100644 index 0000000000000..33331961c0807 --- /dev/null +++ b/packages/kbn-std/src/iteration/map.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as Rx from 'rxjs'; +import { mapTo } from 'rxjs/operators'; + +import { asyncMap, asyncMapWithLimit } from './map'; +import { list } from './test_helpers'; + +jest.mock('./observable'); +const mapWithLimit$: jest.Mock = jest.requireMock('./observable').mapWithLimit$; +mapWithLimit$.mockImplementation(jest.requireActual('./observable').mapWithLimit$); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('asyncMapWithLimit', () => { + it('calls mapWithLimit$ and resolves with properly sorted results', async () => { + const iter = list(10); + const limit = 5; + const fn = jest.fn((n) => (n % 2 ? Rx.timer(n) : Rx.timer(n * 4)).pipe(mapTo(n))); + const result = await asyncMapWithLimit(iter, limit, fn); + + expect(result).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + ] + `); + + expect(mapWithLimit$).toHaveBeenCalledTimes(1); + expect(mapWithLimit$).toHaveBeenCalledWith(iter, limit, expect.any(Function)); + }); + + it.each([ + [list(0), []] as const, + [list(1), ['foo']] as const, + [list(2), ['foo', 'foo']] as const, + ])('resolves when iterator is %p', async (input, output) => { + await expect(asyncMapWithLimit(input, 100, async () => 'foo')).resolves.toEqual(output); + }); +}); + +describe('asyncMap', () => { + it('calls mapWithLimit$ without limit and resolves with undefined when it completes', async () => { + const iter = list(10); + const fn = jest.fn((n) => (n % 2 ? Rx.timer(n) : Rx.timer(n * 4)).pipe(mapTo(n))); + const result = await asyncMap(iter, fn); + + expect(result).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + ] + `); + + expect(mapWithLimit$).toHaveBeenCalledTimes(1); + expect(mapWithLimit$).toHaveBeenCalledWith(iter, Infinity, expect.any(Function)); + }); +}); diff --git a/packages/kbn-std/src/iteration/map.ts b/packages/kbn-std/src/iteration/map.ts new file mode 100644 index 0000000000000..4c8d65df57f37 --- /dev/null +++ b/packages/kbn-std/src/iteration/map.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { from } from 'rxjs'; +import { toArray } from 'rxjs/operators'; +import { lastValueFrom } from '../rxjs_7'; + +import { IterableInput, AsyncMapFn, AsyncMapResult } from './types'; +import { mapWithLimit$ } from './observable'; + +const getAllResults = (input: AsyncMapResult) => lastValueFrom(from(input).pipe(toArray())); + +/** + * Creates a promise whose values is the array of results produced by calling `fn` for + * each item in `iterable`. `fn` can return either a Promise or Observable. If `fn` + * returns observables then they will properly abort if an error occurs. + * + * The result array follows the order of the input iterable, even though the calls + * to `fn` may not. (so avoid side effects) + * + * @param iterable Items to iterate + * @param fn Function to call for each item. Result is added/concatenated into the result array in place of the input value + */ +export async function asyncMap(iterable: IterableInput, fn: AsyncMapFn) { + return await asyncMapWithLimit(iterable, Infinity, fn); +} + +/** + * Creates a promise whose values is the array of results produced by calling `fn` for + * each item in `iterable`. `fn` can return either a Promise or Observable. If `fn` + * returns observables then they will properly abort if an error occurs. + * + * The number of concurrent executions of `fn` is limited by `limit`. + * + * The result array follows the order of the input iterable, even though the calls + * to `fn` may not. (so avoid side effects) + * + * @param iterable Items to iterate + * @param limit Maximum number of operations to run in parallel + * @param fn Function to call for each item. Result is added/concatenated into the result array in place of the input value + */ +export async function asyncMapWithLimit( + iterable: IterableInput, + limit: number, + fn: AsyncMapFn +) { + const results$ = mapWithLimit$( + iterable, + limit, + async (item, i) => [i, await getAllResults(fn(item, i))] as const + ); + + const results = await getAllResults(results$); + + return results + .sort(([a], [b]) => a - b) + .reduce((acc: T2[], [, result]) => acc.concat(result), []); +} diff --git a/packages/kbn-std/src/iteration/observable.test.ts b/packages/kbn-std/src/iteration/observable.test.ts new file mode 100644 index 0000000000000..e84750e08148d --- /dev/null +++ b/packages/kbn-std/src/iteration/observable.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as Rx from 'rxjs'; +import { toArray } from 'rxjs/operators'; +import { lastValueFrom } from '../rxjs_7'; + +import { map$, mapWithLimit$ } from './observable'; +import { list, sleep, generator } from './test_helpers'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('mapWithLimit$', () => { + it('calls the fn for each item and produced each item on the stream with limit 1', async () => { + let maxConcurrency = 0; + let active = 0; + const limit = Math.random() > 0.5 ? 20 : 40; + + const results = await lastValueFrom( + mapWithLimit$(list(100), limit, async (n) => { + active += 1; + if (active > maxConcurrency) { + maxConcurrency = active; + } + await sleep(5); + active -= 1; + return n; + }).pipe(toArray()) + ); + + expect(maxConcurrency).toBe(limit); + expect(results).toHaveLength(100); + for (const [n, i] of results.entries()) { + expect(n).toBe(i); + } + }); + + it.each([ + ['empty array', [], []] as const, + ['empty generator', generator(0), []] as const, + ['generator', generator(5), [0, 1, 2, 3, 4]] as const, + ['set', new Set([5, 4, 3, 2, 1]), [5, 4, 3, 2, 1]] as const, + ['observable', Rx.of(1, 2, 3, 4, 5), [1, 2, 3, 4, 5]] as const, + ])('works with %p', async (_, iter, expected) => { + const mock = jest.fn(async (n) => n); + const results = await lastValueFrom(mapWithLimit$(iter, 1, mock).pipe(toArray())); + expect(results).toEqual(expected); + }); +}); + +describe('map$', () => { + it('applies no limit to mapWithLimit$', async () => { + let maxConcurrency = 0; + let active = 0; + + const results = await lastValueFrom( + map$(list(100), async (n) => { + active += 1; + if (active > maxConcurrency) { + maxConcurrency = active; + } + await sleep(5); + active -= 1; + return n; + }).pipe(toArray()) + ); + + expect(maxConcurrency).toBe(100); + expect(results).toHaveLength(100); + for (const [n, i] of results.entries()) { + expect(n).toBe(i); + } + }); +}); diff --git a/packages/kbn-std/src/iteration/observable.ts b/packages/kbn-std/src/iteration/observable.ts new file mode 100644 index 0000000000000..d11bdd44e52d5 --- /dev/null +++ b/packages/kbn-std/src/iteration/observable.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { from } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IterableInput, AsyncMapFn } from './types'; + +/** + * Creates an observable whose values are the result of calling `fn` for each + * item in `iterable`. `fn` can return either a Promise or an Observable. If + * `fn` returns observables then they will properly abort if an error occurs. + * + * Results are emitted as soon as they are available so their order is very + * likely to not match their order in the input `array`. + * + * @param iterable Items to iterate + * @param fn Function to call for each item. Result is added/concatenated into the result array in place of the input value + */ +export function map$(iterable: IterableInput, fn: AsyncMapFn) { + return from(iterable).pipe(mergeMap(fn)); +} + +/** + * Creates an observable whose values are the result of calling `fn` for each + * item in `iterable`. `fn` can return either a Promise or an Observable. If + * `fn` returns observables then they will properly abort if an error occurs. + * + * The number of concurrent executions of `fn` is limited by `limit`. + * + * Results are emitted as soon as they are available so their order is very + * likely to not match their order in the input `array`. + * + * @param iterable Items to iterate + * @param limit Maximum number of operations to run in parallel + * @param fn Function to call for each item. Result is added/concatenated into the result array in place of the input value + */ +export function mapWithLimit$( + iterable: IterableInput, + limit: number, + fn: AsyncMapFn +) { + return from(iterable).pipe(mergeMap(fn, limit)); +} diff --git a/packages/kbn-std/src/iteration/test_helpers.ts b/packages/kbn-std/src/iteration/test_helpers.ts new file mode 100644 index 0000000000000..e5f7699b090ce --- /dev/null +++ b/packages/kbn-std/src/iteration/test_helpers.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const list = (size: number) => Array.from({ length: size }, (_, i) => i); + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export const generator = function* (size: number) { + for (const n of list(size)) { + yield n; + } +}; diff --git a/packages/kbn-std/src/iteration/types.ts b/packages/kbn-std/src/iteration/types.ts new file mode 100644 index 0000000000000..6e0bfd9f22d7f --- /dev/null +++ b/packages/kbn-std/src/iteration/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Subscribable } from 'rxjs'; + +export type IterableInput = Iterable | Subscribable; +export type AsyncMapResult = Promise | Subscribable; +export type AsyncMapFn = (item: T1, i: number) => AsyncMapResult; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts index 2ad8da7df8fbe..755bb5f946e4f 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts @@ -7,9 +7,7 @@ */ import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import glob from 'glob'; +import del from 'del'; import { kibanaServerTestUser } from '@kbn/test'; import { kibanaPackageJson as pkg } from '@kbn/utils'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; @@ -18,15 +16,8 @@ import { Root } from '../../../root'; const LOG_FILE_PREFIX = 'migration_test_multiple_es_nodes'; -const asyncUnlink = Util.promisify(Fs.unlink); - async function removeLogFile() { - glob(Path.join(__dirname, `${LOG_FILE_PREFIX}_*.log`), (err, files) => { - files.forEach(async (file) => { - // ignore errors if it doesn't exist - await asyncUnlink(file).catch(() => void 0); - }); - }); + await del([Path.join(__dirname, `${LOG_FILE_PREFIX}_*.log`)], { force: true }); } function extractSortNumberFromId(id: string): number { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts index b4a58db1cf8ce..11c5b33c0fd3d 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts @@ -7,9 +7,7 @@ */ import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import glob from 'glob'; +import del from 'del'; import { esTestConfig, kibanaServerTestUser } from '@kbn/test'; import { kibanaPackageJson as pkg } from '@kbn/utils'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; @@ -19,15 +17,8 @@ import type { Root } from '../../../root'; const LOG_FILE_PREFIX = 'migration_test_multiple_kibana_nodes'; -const asyncUnlink = Util.promisify(Fs.unlink); - async function removeLogFiles() { - glob(Path.join(__dirname, `${LOG_FILE_PREFIX}_*.log`), (err, files) => { - files.forEach(async (file) => { - // ignore errors if it doesn't exist - await asyncUnlink(file).catch(() => void 0); - }); - }); + await del([Path.join(__dirname, `${LOG_FILE_PREFIX}_*.log`)], { force: true }); } function extractSortNumberFromId(id: string): number { diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 7fd46e2f6cc44..ee6ec77e63d9c 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -19,7 +19,7 @@ import type { StartServicesAccessor, } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { Subject } from 'rxjs'; +import { map$ } from '@kbn/std'; import { StreamingResponseHandler, BatchRequestData, @@ -208,23 +208,15 @@ export class BfetchServerPlugin >(path, (request) => { const handlerInstance = handler(request); return { - getResponseStream: ({ batch }) => { - const subject = new Subject>(); - let cnt = batch.length; - batch.forEach(async (batchItem, id) => { + getResponseStream: ({ batch }) => + map$(batch, async (batchItem, id) => { try { const result = await handlerInstance.onBatchItem(batchItem); - subject.next({ id, result }); - } catch (err) { - const error = normalizeError(err); - subject.next({ id, error }); - } finally { - cnt--; - if (!cnt) subject.complete(); + return { id, result }; + } catch (error) { + return { id, error: normalizeError(error) }; } - }); - return subject; - }, + }), }; }); }; diff --git a/src/plugins/vis_type_timelion/server/lib/reduce.js b/src/plugins/vis_type_timelion/server/lib/reduce.js index b4fd689028652..65fc4b61a2926 100644 --- a/src/plugins/vis_type_timelion/server/lib/reduce.js +++ b/src/plugins/vis_type_timelion/server/lib/reduce.js @@ -7,6 +7,7 @@ */ import _ from 'lodash'; +import { asyncMap } from '@kbn/std'; function allSeriesContainKey(seriesList, key) { const containsKeyInitialValue = true; @@ -48,16 +49,17 @@ async function pairwiseReduce(left, right, fn) { }); // pairwise reduce seriesLists - const pairwiseSeriesList = { type: 'seriesList', list: [] }; - left.list.forEach(async (leftSeries) => { - const first = { type: 'seriesList', list: [leftSeries] }; - const second = { type: 'seriesList', list: [indexedList[leftSeries[pairwiseField]]] }; - const reducedSeriesList = await reduce([first, second], fn); - const reducedSeries = reducedSeriesList.list[0]; - reducedSeries.label = leftSeries[pairwiseField]; - pairwiseSeriesList.list.push(reducedSeries); - }); - return pairwiseSeriesList; + return { + type: 'seriesList', + list: await asyncMap(left.list, async (leftSeries) => { + const first = { type: 'seriesList', list: [leftSeries] }; + const second = { type: 'seriesList', list: [indexedList[leftSeries[pairwiseField]]] }; + const reducedSeriesList = await reduce([first, second], fn); + const reducedSeries = reducedSeriesList.list[0]; + reducedSeries.label = leftSeries[pairwiseField]; + return reducedSeries; + }), + }; } /** diff --git a/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx index 4701d07ab83e6..cc557ff274fa1 100644 --- a/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx @@ -11,6 +11,7 @@ import classNames from 'classnames'; import { compact, uniqBy, map, every, isUndefined } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { asyncForEach } from '@kbn/std'; import { EuiPopoverProps, EuiIcon, keys, htmlIdGenerator } from '@elastic/eui'; import { PersistedState } from '../../../../../../visualizations/public'; @@ -127,13 +128,14 @@ export class VisLegend extends PureComponent { new Promise(async (resolve, reject) => { try { const filterableLabels = new Set(); - items.forEach(async (item) => { + await asyncForEach(items, async (item) => { const canFilter = await this.canFilter(item); if (canFilter) { filterableLabels.add(item.label); } }); + this.setState( { filterableLabels, diff --git a/x-pack/plugins/event_log/server/es/init.ts b/x-pack/plugins/event_log/server/es/init.ts index e2769e39b28ff..bc1b36ab3e375 100644 --- a/x-pack/plugins/event_log/server/es/init.ts +++ b/x-pack/plugins/event_log/server/es/init.ts @@ -7,6 +7,7 @@ import { IndicesAlias, IndicesIndexStatePrefixedSettings } from '@elastic/elasticsearch/api/types'; import { estypes } from '@elastic/elasticsearch'; +import { asyncForEach } from '@kbn/std'; import { getIlmPolicy, getIndexTemplate } from './documents'; import { EsContext } from './context'; @@ -56,7 +57,7 @@ class EsInitializationSteps { this.esContext.logger.error(`error getting existing index templates - ${err.message}`); } - Object.keys(indexTemplates).forEach(async (indexTemplateName: string) => { + asyncForEach(Object.keys(indexTemplates), async (indexTemplateName: string) => { try { const hidden: string | boolean = indexTemplates[indexTemplateName]?.settings?.index?.hidden; // Check to see if this index template is hidden @@ -93,8 +94,7 @@ class EsInitializationSteps { // should not block the rest of initialization, log the error and move on this.esContext.logger.error(`error getting existing indices - ${err.message}`); } - - Object.keys(indices).forEach(async (indexName: string) => { + asyncForEach(Object.keys(indices), async (indexName: string) => { try { const hidden: string | boolean | undefined = (indices[indexName] ?.settings as IndicesIndexStatePrefixedSettings)?.index?.hidden; @@ -127,7 +127,7 @@ class EsInitializationSteps { // should not block the rest of initialization, log the error and move on this.esContext.logger.error(`error getting existing index aliases - ${err.message}`); } - Object.keys(indexAliases).forEach(async (indexName: string) => { + asyncForEach(Object.keys(indexAliases), async (indexName: string) => { try { const aliases = indexAliases[indexName]?.aliases; const hasNotHiddenAliases: boolean = Object.keys(aliases).some((alias: string) => { diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index aa2bd55e24999..23c389f2a5331 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -7,6 +7,7 @@ import type { ListId, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; +import { asyncForEach } from '@kbn/std'; import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; @@ -80,7 +81,7 @@ export const deleteFoundExceptionListItems = async ({ namespaceType: NamespaceType; }): Promise => { const savedObjectType = getSavedObjectType({ namespaceType }); - ids.forEach(async (id) => { + await asyncForEach(ids, async (id) => { try { await savedObjectsClient.delete(savedObjectType, id); } catch (err) { diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts index a2133f4f2521b..2861e93848ac9 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.ts @@ -7,6 +7,7 @@ import type { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; +import { asyncMap } from '@kbn/std'; import { getIndexPatternService } from './kibana_services'; import { indexPatterns } from '../../../../src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, ES_GEO_FIELD_TYPES } from '../common/constants'; @@ -32,18 +33,17 @@ export function getGeoTileAggNotSupportedReason(field: IndexPatternField): strin export async function getIndexPatternsFromIds( indexPatternIds: string[] = [] ): Promise { - const promises: IndexPattern[] = []; - indexPatternIds.forEach(async (indexPatternId) => { + const results = await asyncMap(indexPatternIds, async (indexPatternId) => { try { - // @ts-ignore - promises.push(getIndexPatternService().get(indexPatternId)); + return (await getIndexPatternService().get(indexPatternId)) as IndexPattern; } catch (error) { // Unable to load index pattern, better to not throw error so map can render // Error will be surfaced by layer since it too will be unable to locate the index pattern return null; } }); - return await Promise.all(promises); + + return results.filter((r): r is IndexPattern => r !== null); } export function getTermsFields(fields: IndexPatternField[]): IndexPatternField[] { diff --git a/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts b/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts index 11f12541bda0d..fbf59fc28af2f 100644 --- a/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts +++ b/x-pack/plugins/metrics_entities/server/services/uninstall_transforms.ts @@ -6,6 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; +import { asyncForEach } from '@kbn/std'; import { Transforms } from '../modules/types'; import type { Logger } from '../../../../../src/core/server'; @@ -35,7 +36,7 @@ export const uninstallTransforms = async ({ suffix, transforms, }: UninstallTransformsOptions): Promise => { - transforms.forEach(async (transform) => { + await asyncForEach(transforms, async (transform) => { const { id } = transform; const computedId = computeTransformId({ id, prefix, suffix }); const exists = await getTransformExists(esClient, computedId); diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 2c12b9f96f0db..caed130543acc 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -8,6 +8,7 @@ import { isEmpty, uniqueId } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; import { useRouteMatch } from 'react-router-dom'; +import { asyncForEach } from '@kbn/std'; import { Alert } from '../../../alerting/common'; import { getDataHandler } from '../data_handler'; import { FETCH_STATUS } from '../hooks/use_fetcher'; @@ -53,7 +54,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode useEffect( () => { if (!isExploratoryView) - apps.forEach(async (app) => { + asyncForEach(apps, async (app) => { try { const updateState = ({ hasData, diff --git a/x-pack/plugins/rollup/server/collectors/helpers.ts b/x-pack/plugins/rollup/server/collectors/helpers.ts index 1d1a8755aa568..b6e5bc190d972 100644 --- a/x-pack/plugins/rollup/server/collectors/helpers.ts +++ b/x-pack/plugins/rollup/server/collectors/helpers.ts @@ -168,7 +168,7 @@ export async function fetchRollupVisualizations( const visualizations = get(savedVisualizationsList, 'hits.hits', []); const sort = savedVisualizationsList.hits.hits[savedVisualizationsList.hits.hits.length - 1].sort; - visualizations.forEach(async (visualization: any) => { + visualizations.forEach((visualization: any) => { const references: Array<{ name: string; id: string; type: string }> | undefined = get( visualization, '_source.references' @@ -193,7 +193,7 @@ export async function fetchRollupVisualizations( } } } - }, [] as string[]); + }); if (savedVisualizationsList.hits.hits.length < ES_MAX_RESULT_WINDOW_DEFAULT_VALUE) { break; diff --git a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index ba3b29a92fd50..39db911710a16 100644 --- a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -10,6 +10,7 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { asyncForEach } from '@kbn/std'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { NotificationsStart } from 'src/core/public'; @@ -81,7 +82,7 @@ export class ConfirmDeleteUsers extends Component { private deleteUsers = () => { const { usersToDelete, callback, userAPIClient, notifications } = this.props; const errors: string[] = []; - usersToDelete.forEach(async (username) => { + asyncForEach(usersToDelete, async (username) => { try { await userAPIClient.deleteUser(username); notifications.toasts.addSuccess( @@ -99,6 +100,7 @@ export class ConfirmDeleteUsers extends Component { ) ); } + }).then(() => { if (callback) { callback(usersToDelete, errors); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts index b4b6e3c824205..5003dbf0279e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { asyncForEach } from '@kbn/std'; import { DeleteRuleOptions } from './types'; export const deleteRules = async ({ @@ -14,5 +15,7 @@ export const deleteRules = async ({ id, }: DeleteRuleOptions) => { await rulesClient.delete({ id }); - ruleStatuses.forEach(async (obj) => ruleStatusClient.delete(obj.id)); + await asyncForEach(ruleStatuses, async (obj) => { + await ruleStatusClient.delete(obj.id); + }); }; diff --git a/x-pack/test/accessibility/apps/helpers.ts b/x-pack/test/accessibility/apps/helpers.ts index cdffd4fabaf8e..18e3a51a2d268 100644 --- a/x-pack/test/accessibility/apps/helpers.ts +++ b/x-pack/test/accessibility/apps/helpers.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { asyncForEach } from '@kbn/std'; + // This function clears all pipelines to ensure that there in an empty state before starting each test. export async function deleteAllPipelines(client: any, logger: any) { const pipelines = await client.ingest.getPipeline(); const pipeLineIds = Object.keys(pipelines.body); await logger.debug(pipelines); if (pipeLineIds.length > 0) { - pipeLineIds.forEach(async (newId: any) => { + await asyncForEach(pipeLineIds, async (newId: any) => { await client.ingest.deletePipeline({ id: newId }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts index 358e667bcb05b..59393f7a4acf1 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -34,8 +34,8 @@ export default function ({ getService }: FtrProviderContext) { const scheduleEvery = 10000; // fake monitor checks every 10s let dateRange: { start: string; end: string }; - [true, false].forEach(async (includeTimespan: boolean) => { - [true, false].forEach(async (includeObserver: boolean) => { + [true, false].forEach((includeTimespan: boolean) => { + [true, false].forEach((includeObserver: boolean) => { describe(`with timespans=${includeTimespan} and observer=${includeObserver}`, async () => { before(async () => { const promises: Array> = []; diff --git a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts index bda845d62fd0b..f665a0aa62cf5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -90,7 +91,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should list the logs and metrics datastream', async function () { - namespaces.forEach(async (namespace) => { + await asyncForEach(namespaces, async (namespace) => { const resLogsDatastream = await es.transport.request({ method: 'GET', path: `/_data_stream/${logsTemplateName}-${namespace}`, @@ -108,7 +109,7 @@ export default function (providerContext: FtrProviderContext) { it('after update, it should have rolled over logs datastream because mappings are not compatible and not metrics', async function () { await installPackage(pkgUpdateKey); - namespaces.forEach(async (namespace) => { + await asyncForEach(namespaces, async (namespace) => { const resLogsDatastream = await es.transport.request({ method: 'GET', path: `/_data_stream/${logsTemplateName}-${namespace}`, @@ -123,7 +124,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should be able to upgrade a package after a rollover', async function () { - namespaces.forEach(async (namespace) => { + await asyncForEach(namespaces, async (namespace) => { await es.transport.request({ method: 'POST', path: `/${logsTemplateName}-${namespace}/_rollover`, diff --git a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js index 1ce4ccdcec97f..b8ea04a17fe6a 100644 --- a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js +++ b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js @@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }) { const GEO_POINT = 'geo_point'; const pointGeojsonFiles = ['point.json', 'multi_point.json']; - pointGeojsonFiles.forEach(async (pointFile) => { + pointGeojsonFiles.forEach((pointFile) => { it(`should index with type geo_point for file: ${pointFile}`, async () => { if (!(await browser.checkBrowserPermission('clipboard-read'))) { return; @@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }) { 'multi_polygon.json', 'polygon.json', ]; - nonPointGeojsonFiles.forEach(async (shapeFile) => { + nonPointGeojsonFiles.forEach((shapeFile) => { it(`should index with type geo_shape for file: ${shapeFile}`, async () => { if (!(await browser.checkBrowserPermission('clipboard-read'))) { return; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 661b452855a86..2ce771f7b993f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; import { generateUniqueKey } from '../../lib/get_test_data'; @@ -28,7 +29,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } async function deleteAlerts(alertIds: string[]) { - alertIds.forEach(async (alertId: string) => { + await asyncForEach(alertIds, async (alertId: string) => { await supertest .delete(`/api/alerting/rule/${alertId}`) .set('kbn-xsrf', 'foo') From 34581ff1edcc59acca9cf57e8cbcd5d8602d439e Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Sep 2021 13:20:59 -0700 Subject: [PATCH 37/39] skip flaky suite (#103430) --- .../test/functional/apps/dashboard/reporting/download_csv.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index 2d6812c61e554..df45c3e3ca2ca 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -52,7 +52,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel }; - describe('Download CSV', () => { + // Failing: See https://github.com/elastic/kibana/issues/103430 + describe.skip('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await browser.setWindowSize(1600, 850); From db5cf957243cdcfaaf32fc1899ed0b74f226015d Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 14 Sep 2021 21:58:25 +0100 Subject: [PATCH 38/39] Verification code CLI (#111707) * Add verification code CLI * Added suggestion from code review * Fixed types * Added extra test * Added CLI dist scripts * Fixed typo * Write code to data instead of config directory --- scripts/kibana_verification_code.js | 9 ++ .../cli_verification_code.js | 39 +++++++++ src/cli_verification_code/dev.js | 10 +++ src/cli_verification_code/dist.js | 10 +++ .../bin/scripts/kibana-verification-code | 29 +++++++ .../bin/scripts/kibana-verification-code.bat | 35 ++++++++ .../public/single_chars_field.tsx | 5 ++ .../public/verification_code_form.test.tsx | 83 +++++++++++++++++++ .../public/verification_code_form.tsx | 10 ++- .../interactive_setup/server/plugin.ts | 21 +++-- .../server/verification_service.test.ts | 55 ++++++++++++ .../server/verification_service.ts | 49 +++++++++++ 12 files changed, 346 insertions(+), 9 deletions(-) create mode 100644 scripts/kibana_verification_code.js create mode 100644 src/cli_verification_code/cli_verification_code.js create mode 100644 src/cli_verification_code/dev.js create mode 100644 src/cli_verification_code/dist.js create mode 100755 src/dev/build/tasks/bin/scripts/kibana-verification-code create mode 100755 src/dev/build/tasks/bin/scripts/kibana-verification-code.bat create mode 100644 src/plugins/interactive_setup/public/verification_code_form.test.tsx create mode 100644 src/plugins/interactive_setup/server/verification_service.test.ts create mode 100644 src/plugins/interactive_setup/server/verification_service.ts diff --git a/scripts/kibana_verification_code.js b/scripts/kibana_verification_code.js new file mode 100644 index 0000000000000..42e3e54b9d4c8 --- /dev/null +++ b/scripts/kibana_verification_code.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/cli_verification_code/dev'); diff --git a/src/cli_verification_code/cli_verification_code.js b/src/cli_verification_code/cli_verification_code.js new file mode 100644 index 0000000000000..7ed83e8211c3c --- /dev/null +++ b/src/cli_verification_code/cli_verification_code.js @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { kibanaPackageJson, getDataPath } from '@kbn/utils'; +import path from 'path'; +import fs from 'fs'; +import chalk from 'chalk'; + +import Command from '../cli/command'; + +const program = new Command('bin/kibana-verification-code'); + +program + .version(kibanaPackageJson.version) + .description('Tool to get Kibana verification code') + .action(() => { + const fpath = path.join(getDataPath(), 'verification_code'); + try { + const code = fs.readFileSync(fpath).toString(); + console.log( + `Your verification code is: ${chalk.black.bgCyanBright( + ` ${code.substr(0, 3)} ${code.substr(3)} ` + )}` + ); + } catch (error) { + console.log(`Couldn't find verification code. + +If Kibana hasn't been configured yet, restart Kibana to generate a new code. + +Otherwise, you can safely ignore this message and start using Kibana.`); + } + }); + +program.parse(process.argv); diff --git a/src/cli_verification_code/dev.js b/src/cli_verification_code/dev.js new file mode 100644 index 0000000000000..e424f6b7bec91 --- /dev/null +++ b/src/cli_verification_code/dev.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../setup_node_env'); +require('./cli_verification_code'); diff --git a/src/cli_verification_code/dist.js b/src/cli_verification_code/dist.js new file mode 100644 index 0000000000000..6f7e63591a96d --- /dev/null +++ b/src/cli_verification_code/dist.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../setup_node_env/dist'); +require('./cli_verification_code'); diff --git a/src/dev/build/tasks/bin/scripts/kibana-verification-code b/src/dev/build/tasks/bin/scripts/kibana-verification-code new file mode 100755 index 0000000000000..e8214affc23d7 --- /dev/null +++ b/src/dev/build/tasks/bin/scripts/kibana-verification-code @@ -0,0 +1,29 @@ +#!/bin/sh +SCRIPT=$0 + +# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path. +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + # Drop everything prior to -> + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +DIR="$(dirname "${SCRIPT}")/.." +CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"} +NODE="${DIR}/node/bin/node" +test -x "$NODE" +if [ ! -x "$NODE" ]; then + echo "unable to find usable node.js executable." + exit 1 +fi + +if [ -f "${CONFIG_DIR}/node.options" ]; then + KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)" +fi + +NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_verification_code/dist" "$@" diff --git a/src/dev/build/tasks/bin/scripts/kibana-verification-code.bat b/src/dev/build/tasks/bin/scripts/kibana-verification-code.bat new file mode 100755 index 0000000000000..9202244e951e4 --- /dev/null +++ b/src/dev/build/tasks/bin/scripts/kibana-verification-code.bat @@ -0,0 +1,35 @@ +@echo off + +SETLOCAL ENABLEDELAYEDEXPANSION + +set SCRIPT_DIR=%~dp0 +for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI + +set NODE=%DIR%\node\node.exe + +If Not Exist "%NODE%" ( + Echo unable to find usable node.js executable. + Exit /B 1 +) + +set CONFIG_DIR=%KBN_PATH_CONF% +If ["%KBN_PATH_CONF%"] == [] ( + set "CONFIG_DIR=%DIR%\config" +) + +IF EXIST "%CONFIG_DIR%\node.options" ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( + If [!NODE_OPTIONS!] == [] ( + set "NODE_OPTIONS=%%i" + ) Else ( + set "NODE_OPTIONS=!NODE_OPTIONS! %%i" + ) + ) +) + +TITLE Kibana Verification Code +"%NODE%" "%DIR%\src\cli_verification_code\dist" %* + +:finally + +ENDLOCAL diff --git a/src/plugins/interactive_setup/public/single_chars_field.tsx b/src/plugins/interactive_setup/public/single_chars_field.tsx index 8d5cd2854c0aa..18ecd38e6483a 100644 --- a/src/plugins/interactive_setup/public/single_chars_field.tsx +++ b/src/plugins/interactive_setup/public/single_chars_field.tsx @@ -12,6 +12,7 @@ import React, { useEffect, useRef } from 'react'; import useList from 'react-use/lib/useList'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; +import { i18n } from '@kbn/i18n'; import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; export interface SingleCharsFieldProps { @@ -124,6 +125,10 @@ export const SingleCharsField: FunctionComponent = ({ maxLength={1} isInvalid={isInvalid} style={{ textAlign: 'center' }} + aria-label={i18n.translate('interactiveSetup.singleCharsField.digitLabel', { + defaultMessage: 'Digit {index}', + values: { index: i + 1 }, + })} /> ); diff --git a/src/plugins/interactive_setup/public/verification_code_form.test.tsx b/src/plugins/interactive_setup/public/verification_code_form.test.tsx new file mode 100644 index 0000000000000..2b7f1a86fcfad --- /dev/null +++ b/src/plugins/interactive_setup/public/verification_code_form.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { coreMock } from 'src/core/public/mocks'; + +import { Providers } from './plugin'; +import { VerificationCodeForm } from './verification_code_form'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('VerificationCodeForm', () => { + jest.setTimeout(20_000); + + it('calls enrollment API when submitting form', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.post.mockResolvedValue({}); + + const onSuccess = jest.fn(); + + const { findByRole, findByLabelText } = render( + + + + ); + fireEvent.input(await findByLabelText('Digit 1'), { + target: { value: '1' }, + }); + fireEvent.input(await findByLabelText('Digit 2'), { + target: { value: '2' }, + }); + fireEvent.input(await findByLabelText('Digit 3'), { + target: { value: '3' }, + }); + fireEvent.input(await findByLabelText('Digit 4'), { + target: { value: '4' }, + }); + fireEvent.input(await findByLabelText('Digit 5'), { + target: { value: '5' }, + }); + fireEvent.input(await findByLabelText('Digit 6'), { + target: { value: '6' }, + }); + fireEvent.click(await findByRole('button', { name: 'Verify', hidden: true })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/interactive_setup/verify', { + body: JSON.stringify({ code: '123456' }), + }); + expect(onSuccess).toHaveBeenCalled(); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const onSuccess = jest.fn(); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Verify', hidden: true })); + + await findAllByText(/Enter a verification code/i); + + fireEvent.input(await findByLabelText('Digit 1'), { + target: { value: '1' }, + }); + + await findAllByText(/Enter all six digits/i); + }); +}); diff --git a/src/plugins/interactive_setup/public/verification_code_form.tsx b/src/plugins/interactive_setup/public/verification_code_form.tsx index 8f4a9ea8c5d01..8bea8229baec3 100644 --- a/src/plugins/interactive_setup/public/verification_code_form.tsx +++ b/src/plugins/interactive_setup/public/verification_code_form.tsx @@ -9,6 +9,7 @@ import { EuiButton, EuiCallOut, + EuiCode, EuiEmptyPrompt, EuiForm, EuiFormRow, @@ -69,8 +70,8 @@ export const VerificationCodeForm: FunctionComponent }), }); } catch (error) { - if (error.response?.status === 403) { - form.setError('code', error.body?.message); + if ((error as IHttpFetchError).response?.status === 403) { + form.setError('code', (error as IHttpFetchError).body?.message); return; } else { throw error; @@ -111,7 +112,10 @@ export const VerificationCodeForm: FunctionComponent

./bin/kibana-verification-code, + }} />

diff --git a/src/plugins/interactive_setup/server/plugin.ts b/src/plugins/interactive_setup/server/plugin.ts index 8c57b6e8514c0..2c3b517e78c5f 100644 --- a/src/plugins/interactive_setup/server/plugin.ts +++ b/src/plugins/interactive_setup/server/plugin.ts @@ -17,7 +17,7 @@ import type { ConfigSchema, ConfigType } from './config'; import { ElasticsearchService } from './elasticsearch_service'; import { KibanaConfigWriter } from './kibana_config_writer'; import { defineRoutes } from './routes'; -import { VerificationCode } from './verification_code'; +import { VerificationService } from './verification_service'; // List of the Elasticsearch hosts Kibana uses by default. const DEFAULT_ELASTICSEARCH_HOSTS = [ @@ -29,7 +29,7 @@ const DEFAULT_ELASTICSEARCH_HOSTS = [ export class InteractiveSetupPlugin implements PrebootPlugin { readonly #logger: Logger; readonly #elasticsearch: ElasticsearchService; - readonly #verificationCode: VerificationCode; + readonly #verification: VerificationService; #elasticsearchConnectionStatusSubscription?: Subscription; @@ -47,7 +47,7 @@ export class InteractiveSetupPlugin implements PrebootPlugin { this.#elasticsearch = new ElasticsearchService( this.initializerContext.logger.get('elasticsearch') ); - this.#verificationCode = new VerificationCode( + this.#verification = new VerificationService( this.initializerContext.logger.get('verification') ); } @@ -73,6 +73,14 @@ export class InteractiveSetupPlugin implements PrebootPlugin { return; } + const verificationCode = this.#verification.setup(); + if (!verificationCode) { + this.#logger.error( + 'Interactive setup mode could not be activated. Ensure Kibana has permission to write to its config folder.' + ); + return; + } + let completeSetup: (result: { shouldReloadConfig: boolean }) => void; core.preboot.holdSetupUntilResolved( 'Validating Elasticsearch connection configuration…', @@ -93,6 +101,7 @@ export class InteractiveSetupPlugin implements PrebootPlugin { elasticsearch: core.elasticsearch, connectionCheckInterval: this.#getConfig().connectionCheck.interval, }); + this.#elasticsearchConnectionStatusSubscription = elasticsearch.connectionStatus$.subscribe( (status) => { if (status === ElasticsearchConnectionStatus.Configured) { @@ -104,10 +113,9 @@ export class InteractiveSetupPlugin implements PrebootPlugin { this.#logger.debug( 'Starting interactive setup mode since Kibana cannot to connect to Elasticsearch at http://localhost:9200.' ); - const { code } = this.#verificationCode; const pathname = core.http.basePath.prepend('/'); const { protocol, hostname, port } = core.http.getServerInfo(); - const url = `${protocol}://${hostname}:${port}${pathname}?code=${code}`; + const url = `${protocol}://${hostname}:${port}${pathname}?code=${verificationCode.code}`; // eslint-disable-next-line no-console console.log(` @@ -135,7 +143,7 @@ Go to ${chalk.cyanBright.underline(url)} to get started. preboot: { ...core.preboot, completeSetup }, kibanaConfigWriter: new KibanaConfigWriter(configPath, this.#logger.get('kibana-config')), elasticsearch, - verificationCode: this.#verificationCode, + verificationCode, getConfig: this.#getConfig.bind(this), }); }); @@ -155,5 +163,6 @@ Go to ${chalk.cyanBright.underline(url)} to get started. } this.#elasticsearch.stop(); + this.#verification.stop(); } } diff --git a/src/plugins/interactive_setup/server/verification_service.test.ts b/src/plugins/interactive_setup/server/verification_service.test.ts new file mode 100644 index 0000000000000..1721362d1c635 --- /dev/null +++ b/src/plugins/interactive_setup/server/verification_service.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; + +import { loggingSystemMock } from 'src/core/server/mocks'; + +import { VerificationCode } from './verification_code'; +import { VerificationService } from './verification_service'; + +jest.mock('fs'); +jest.mock('@kbn/utils', () => ({ + getDataPath: jest.fn().mockReturnValue('/data/'), +})); + +const loggerMock = loggingSystemMock.createLogger(); + +describe('VerificationService', () => { + describe('setup()', () => { + it('should generate verification code', () => { + const service = new VerificationService(loggerMock); + const setup = service.setup(); + expect(setup).toBeInstanceOf(VerificationCode); + }); + + it('should write verification code to disk', () => { + const service = new VerificationService(loggerMock); + const setup = service.setup()!; + expect(fs.writeFileSync).toHaveBeenCalledWith('/data/verification_code', setup.code); + }); + + it('should not return verification code if cannot write to disk', () => { + const service = new VerificationService(loggerMock); + (fs.writeFileSync as jest.Mock).mockImplementationOnce(() => { + throw new Error('Write error'); + }); + const setup = service.setup(); + expect(fs.writeFileSync).toHaveBeenCalledWith('/data/verification_code', expect.anything()); + expect(setup).toBeUndefined(); + }); + }); + + describe('stop()', () => { + it('should remove verification code from disk', () => { + const service = new VerificationService(loggerMock); + service.stop(); + expect(fs.unlinkSync).toHaveBeenCalledWith('/data/verification_code'); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/verification_service.ts b/src/plugins/interactive_setup/server/verification_service.ts new file mode 100644 index 0000000000000..b158b23bd3b6d --- /dev/null +++ b/src/plugins/interactive_setup/server/verification_service.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; +import path from 'path'; + +import { getDataPath } from '@kbn/utils'; +import type { Logger } from 'src/core/server'; + +import { getDetailedErrorMessage } from './errors'; +import { VerificationCode } from './verification_code'; + +export class VerificationService { + private fileName: string; + + constructor(private readonly logger: Logger) { + this.fileName = path.join(getDataPath(), 'verification_code'); + } + + public setup() { + const verificationCode = new VerificationCode(this.logger); + + try { + fs.writeFileSync(this.fileName, verificationCode.code); + this.logger.debug(`Successfully wrote verification code to ${this.fileName}`); + return verificationCode; + } catch (error) { + this.logger.error( + `Failed to write verification code to ${this.fileName}: ${getDetailedErrorMessage(error)}.` + ); + } + } + + public stop() { + try { + fs.unlinkSync(this.fileName); + this.logger.debug(`Successfully removed ${this.fileName}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + this.logger.error(`Failed to remove ${this.fileName}: ${getDetailedErrorMessage(error)}.`); + } + } + } +} From ef16b6a732d5d33f6883925e80c444ff9642b720 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 14 Sep 2021 16:17:26 -0500 Subject: [PATCH 39/39] [Workplace Search] Fix bug where non-admins shown broken admin dashboard (#112163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add route for non-admins This redirects to /p/sources so I am leaving ‘sources’ off here in case we change the landing page for the Private Dashboard int he future * Pass Workplace Search user admin status to Product Selector * Pass custom URL to Workplace Search Product Card App Search will remain unchanged. The default URL will be used if no custom URL is passed to the Product Card --- .../enterprise_search/common/constants.ts | 1 + .../components/product_card/product_card.tsx | 5 +-- .../product_selector.test.tsx | 32 ++++++++++++++++--- .../product_selector/product_selector.tsx | 16 ++++++++-- .../applications/enterprise_search/index.tsx | 9 ++++-- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index f162f5810cb61..bc60c917094f8 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -55,6 +55,7 @@ export const WORKPLACE_SEARCH_PLUGIN = { } ), URL: '/app/enterprise_search/workplace_search', + NON_ADMIN_URL: '/app/enterprise_search/workplace_search/p', SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/', }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index c8dd9523f62f4..33b7658788f62 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -28,9 +28,10 @@ interface ProductCardProps { URL: string; }; image: string; + url?: string; } -export const ProductCard: React.FC = ({ product, image }) => { +export const ProductCard: React.FC = ({ product, image, url }) => { const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); const { config } = useValues(KibanaLogic); @@ -68,7 +69,7 @@ export const ProductCard: React.FC = ({ product, image }) => { footer={ sendEnterpriseSearchTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index ee0209c1a124d..c713507083b08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -11,6 +11,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; + import { LicenseCallout } from '../license_callout'; import { ProductCard } from '../product_card'; import { SetupGuideCta } from '../setup_guide'; @@ -18,10 +20,15 @@ import { TrialCallout } from '../trial_callout'; import { ProductSelector } from './'; +const props = { + access: {}, + isWorkplaceSearchAdmin: true, +}; + describe('ProductSelector', () => { it('renders the overview page, product cards, & setup guide CTAs with no host set', () => { setMockValues({ config: { host: '' } }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(ProductCard)).toHaveLength(2); expect(wrapper.find(SetupGuideCta)).toHaveLength(1); @@ -30,12 +37,21 @@ describe('ProductSelector', () => { it('renders the license and trial callouts', () => { setMockValues({ config: { host: 'localhost' } }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(TrialCallout)).toHaveLength(1); expect(wrapper.find(LicenseCallout)).toHaveLength(1); }); + it('passes correct URL when Workplace Search user is not an admin', () => { + setMockValues({ config: { host: '' } }); + const wrapper = shallow(); + + expect(wrapper.find(ProductCard).last().prop('url')).toEqual( + WORKPLACE_SEARCH_PLUGIN.NON_ADMIN_URL + ); + }); + describe('access checks when host is set', () => { beforeEach(() => { setMockValues({ config: { host: 'localhost' } }); @@ -43,7 +59,10 @@ describe('ProductSelector', () => { it('does not render the App Search card if the user does not have access to AS', () => { const wrapper = shallow( - + ); expect(wrapper.find(ProductCard)).toHaveLength(1); @@ -52,7 +71,10 @@ describe('ProductSelector', () => { it('does not render the Workplace Search card if the user does not have access to WS', () => { const wrapper = shallow( - + ); expect(wrapper.find(ProductCard)).toHaveLength(1); @@ -60,7 +82,7 @@ describe('ProductSelector', () => { }); it('does not render any cards if the user does not have access', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(ProductCard)).toHaveLength(0); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index f42a043e7cd81..690122efc2f20 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -34,9 +34,13 @@ interface ProductSelectorProps { hasAppSearchAccess?: boolean; hasWorkplaceSearchAccess?: boolean; }; + isWorkplaceSearchAdmin: boolean; } -export const ProductSelector: React.FC = ({ access }) => { +export const ProductSelector: React.FC = ({ + access, + isWorkplaceSearchAdmin, +}) => { const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; const { config } = useValues(KibanaLogic); @@ -44,6 +48,10 @@ export const ProductSelector: React.FC = ({ access }) => { const shouldShowAppSearchCard = !config.host || hasAppSearchAccess; const shouldShowWorkplaceSearchCard = !config.host || hasWorkplaceSearchAccess; + const WORKPLACE_SEARCH_URL = isWorkplaceSearchAdmin + ? WORKPLACE_SEARCH_PLUGIN.URL + : WORKPLACE_SEARCH_PLUGIN.NON_ADMIN_URL; + return ( @@ -84,7 +92,11 @@ export const ProductSelector: React.FC = ({ access }) => { )} {shouldShowWorkplaceSearchCard && ( - + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index b1aab0dacabde..17387ae482325 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -19,11 +19,12 @@ import { ProductSelector } from './components/product_selector'; import { SetupGuide } from './components/setup_guide'; import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; -export const EnterpriseSearch: React.FC = ({ access = {} }) => { +export const EnterpriseSearch: React.FC = ({ access = {}, workplaceSearch }) => { const { errorConnecting } = useValues(HttpLogic); const { config } = useValues(KibanaLogic); const showErrorConnecting = !!(config.host && errorConnecting); + const isWorkplaceSearchAdmin = !!workplaceSearch?.account?.isAdmin; return ( @@ -31,7 +32,11 @@ export const EnterpriseSearch: React.FC = ({ access = {} }) => { - {showErrorConnecting ? : } + {showErrorConnecting ? ( + + ) : ( + + )} );