From 0712741bb361ecd1683ca5f8e7a7e010e1864617 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 29 May 2020 17:07:45 +0300 Subject: [PATCH 01/69] [SIEM][CASE] Fix callout messages appearance (#67303) Co-authored-by: Elastic Machine --- .../cases/components/callout/index.test.tsx | 37 ++++++++++++++++ .../public/cases/components/callout/index.tsx | 2 +- .../use_push_to_service/index.test.tsx | 43 ++++++++++++++++++- .../components/use_push_to_service/index.tsx | 2 +- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx index c830a6f5e10d5..ee3faeb2ceeb5 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx @@ -60,6 +60,43 @@ describe('CaseCallOut ', () => { ).toBeTruthy(); }); + it('it applies the correct color to button', () => { + const props = { + ...defaultProps, + messages: [ + { + ...defaultProps, + description:

{'one'}

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

{'two'}

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

{'three'}

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

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

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

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

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

{'Endpoints will go here'}

+ +
+ ); +}; + +export const ManagementContainer = memo(() => { + return ( + + + + } + /> + + + ); +}); + +ManagementContainer.displayName = 'ManagementContainer'; diff --git a/x-pack/plugins/siem/public/management/pages/policy/index.tsx b/x-pack/plugins/siem/public/management/pages/policy/index.tsx new file mode 100644 index 0000000000000..5122bbcd5d55d --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { PolicyDetails, PolicyList } from './view'; +import { + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, +} from '../../common/constants'; +import { NotFoundPage } from '../../../app/404'; + +export const PolicyContainer = memo(() => { + return ( + + + + + + ); +}); + +PolicyContainer.displayName = 'PolicyContainer'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts b/x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts rename to x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts index 44be5ddcc003f..7c67dffb8a663 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UIPolicyConfig } from '../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../common/endpoint/types'; /** * A typed Object.entries() function where the keys and values are typed based on the given object diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts index ceb62a9f9ace9..f729dfbd9a29a 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetAgentStatusResponse } from '../../../../../ingest_manager/common/types/rest_spec'; -import { PolicyData, UIPolicyConfig } from '../../../../common/endpoint/types'; -import { ServerApiError } from '../../../common/types'; +import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec'; +import { PolicyData, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../common/types'; import { PolicyDetailsState } from '../../types'; interface ServerReturnedPolicyDetailsData { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts index 01a824ecc7b8e..469b71854dfcc 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts @@ -9,7 +9,7 @@ import { createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction } from './index'; import { policyConfig } from './selectors'; import { clone } from '../../models/policy_details_config'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; describe('policy details: ', () => { let store: Store; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts similarity index 77% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts index 88f090301cfa3..9ccc47f250e4e 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts @@ -5,9 +5,9 @@ */ import { PolicyDetailsState } from '../../types'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export { policyDetailsMiddlewareFactory } from './middleware'; export { PolicyDetailsAction } from './action'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts similarity index 93% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts index 883d8e780ea67..97cdcac0fcae9 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts @@ -16,9 +16,9 @@ import { sendGetFleetAgentStatusForConfig, sendPutDatasource, } from '../policy_list/services/ingest'; -import { NewPolicyData, PolicyData, Immutable } from '../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; -import { ImmutableMiddlewareFactory } from '../../../common/store'; +import { NewPolicyData, PolicyData, Immutable } from '../../../../../../common/endpoint/types'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { ImmutableMiddlewareFactory } from '../../../../../common/store'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { return { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts index 3c943986a72e4..d2a5c1b7e14a3 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts @@ -5,14 +5,17 @@ */ import { createSelector } from 'reselect'; +import { matchPath } from 'react-router-dom'; import { PolicyDetailsState } from '../../types'; import { Immutable, NewPolicyData, PolicyConfig, UIPolicyConfig, -} from '../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; +} from '../../../../../../common/endpoint/types'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { MANAGEMENT_ROUTING_POLICY_DETAILS_PATH } from '../../../../common/constants'; +import { ManagementRoutePolicyDetailsParams } from '../../../../types'; /** Returns the policy details */ export const policyDetails = (state: Immutable) => state.policyItem; @@ -31,22 +34,24 @@ export const policyDetailsForUpdate: ( /** Returns a boolean of whether the user is on the policy details page or not */ export const isOnPolicyDetailsPage = (state: Immutable) => { - if (state.location) { - const pathnameParts = state.location.pathname.split('/'); - return pathnameParts[1] === 'policy' && pathnameParts[2]; - } else { - return false; - } + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + exact: true, + }) !== null + ); }; /** Returns the policyId from the url */ export const policyIdFromParams: (state: Immutable) => string = createSelector( (state) => state.location, (location: PolicyDetailsState['location']) => { - if (location) { - return location.pathname.split('/')[2]; - } - return ''; + return ( + matchPath(location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + exact: true, + })?.params?.policyId ?? '' + ); } ); diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts similarity index 83% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts index bedbcdae3306f..6866bcbf31f89 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData } from '../../../../common/endpoint/types'; -import { ServerApiError } from '../../../common/types'; +import { PolicyData } from '../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../common/types'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts index 9b56062879583..c796edff8aabc 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts @@ -7,19 +7,24 @@ import { PolicyListState } from '../../types'; import { Store, applyMiddleware, createStore } from 'redux'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../ingest_manager/common'; +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common'; import { policyListReducer, initialPolicyListState } from './reducer'; import { policyListMiddlewareFactory } from './middleware'; import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors'; -import { DepsStartMock, depsStartMock } from '../../../common/mock/endpoint'; +import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint'; import { setPolicyListApiMockImplementation } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; -import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../../../common/store/test_utils'; +import { + createSpyMiddleware, + MiddlewareActionSpyHelper, +} from '../../../../../common/store/test_utils'; +import { getManagementUrl } from '../../../../common/routing'; describe('policy list store concerns', () => { + const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); let fakeCoreStart: ReturnType; let depsStart: DepsStartMock; let store: Store; @@ -57,7 +62,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -70,7 +75,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -84,7 +89,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -112,7 +117,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -132,7 +137,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: searchParams, hash: '', }, diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts similarity index 76% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts index a4f51fcf0ec66..e09f80883d888 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts @@ -5,9 +5,9 @@ */ import { PolicyListState } from '../../types'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export { policyListReducer } from './reducer'; export { PolicyListAction } from './action'; export { policyListMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts similarity index 91% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts index 8602ab8170565..6054ec34b2d01 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts @@ -7,8 +7,8 @@ import { GetPolicyListResponse, PolicyListState } from '../../types'; import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; -import { ImmutableMiddlewareFactory } from '../../../common/store'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableMiddlewareFactory } from '../../../../../common/store'; +import { Immutable } from '../../../../../../common/endpoint/types'; export const policyListMiddlewareFactory: ImmutableMiddlewareFactory> = ( coreStart diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts similarity index 89% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts index 80e890602c921..028e46936b293 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts @@ -6,9 +6,9 @@ import { PolicyListState } from '../../types'; import { isOnPolicyListPage } from './selectors'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export const initialPolicyListState = (): PolicyListState => { return { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts index cd6230a6ed3be..c900ceb186f69 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts @@ -6,8 +6,10 @@ import { createSelector } from 'reselect'; import { parse } from 'query-string'; +import { matchPath } from 'react-router-dom'; import { PolicyListState, PolicyListUrlSearchParams } from '../../types'; -import { Immutable } from '../../../../common/endpoint/types'; +import { Immutable } from '../../../../../../common/endpoint/types'; +import { MANAGEMENT_ROUTING_POLICIES_PATH } from '../../../../common/constants'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -24,7 +26,12 @@ export const selectIsLoading = (state: Immutable) => state.isLo export const selectApiError = (state: Immutable) => state.apiError; export const isOnPolicyListPage = (state: Immutable) => { - return state.location?.pathname === '/policy'; + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICIES_PATH, + exact: true, + }) !== null + ); }; const routeLocation = (state: Immutable) => state.location; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts similarity index 94% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts index df61bbe893c58..cbbc5c3c6fdbe 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -5,8 +5,8 @@ */ import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; -import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; +import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; describe('ingest service', () => { let http: ReturnType; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts similarity index 95% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts index 312a3f7491ab2..db482e2a6bdb6 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -9,9 +9,9 @@ import { GetDatasourcesRequest, GetAgentStatusResponse, DATASOURCE_SAVED_OBJECT_TYPE, -} from '../../../../../../ingest_manager/common'; +} from '../../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; -import { NewPolicyData } from '../../../../../common/endpoint/types'; +import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts similarity index 93% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b8fac21b76a26..2c495202dc75b 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -6,7 +6,7 @@ import { HttpStart } from 'kibana/public'; import { INGEST_API_DATASOURCES } from './services/ingest'; -import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; const generator = new EndpointDocGenerator('policy-list'); diff --git a/x-pack/plugins/siem/public/endpoint_policy/types.ts b/x-pack/plugins/siem/public/management/pages/policy/types.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/types.ts rename to x-pack/plugins/siem/public/management/pages/policy/types.ts index ba42140589789..f8cc0d5cd0508 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/types.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/types.ts @@ -10,14 +10,14 @@ import { MalwareFields, UIPolicyConfig, AppLocation, -} from '../../common/endpoint/types'; -import { ServerApiError } from '../common/types'; +} from '../../../../common/endpoint/types'; +import { ServerApiError } from '../../../common/types'; import { GetAgentStatusResponse, GetDatasourcesResponse, GetOneDatasourceResponse, UpdateDatasourceResponse, -} from '../../../ingest_manager/common'; +} from '../../../../../ingest_manager/common'; /** * Policy list store state diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/agents_summary.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/agents_summary.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/agents_summary.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/agents_summary.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/index.ts b/x-pack/plugins/siem/public/management/pages/policy/view/index.ts similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/view/index.ts diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx index 5d736da4e5635..01e12e6c767a6 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx @@ -8,12 +8,20 @@ import React from 'react'; import { mount } from 'enzyme'; import { PolicyDetails } from './policy_details'; -import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; -import { createAppRootMockRenderer } from '../../common/mock/endpoint'; +import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { getManagementUrl } from '../../../common/routing'; describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType['find']>; + const policyDetailsPathUrl = getManagementUrl({ + name: 'policyDetails', + policyId: '1', + excludePrefix: true, + }); + const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); + const policyListPathUrlWithPrefix = getManagementUrl({ name: 'policyList' }); const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); const generator = new EndpointDocGenerator(); const { history, AppWrapper, coreStart } = createAppRootMockRenderer(); @@ -33,7 +41,7 @@ describe('Policy Details', () => { describe('when displayed with invalid id', () => { beforeEach(() => { http.get.mockReturnValue(Promise.reject(new Error('policy not found'))); - history.push('/policy/1'); + history.push(policyDetailsPathUrl); policyView = render(); }); @@ -77,7 +85,7 @@ describe('Policy Details', () => { return Promise.reject(new Error('unknown API call!')); }); - history.push('/policy/1'); + history.push(policyDetailsPathUrl); policyView = render(); }); @@ -89,7 +97,7 @@ describe('Policy Details', () => { const backToListButton = pageHeaderLeft.find('EuiButtonEmpty'); expect(backToListButton.prop('iconType')).toBe('arrowLeft'); - expect(backToListButton.prop('href')).toBe('/mock/app/endpoint/policy'); + expect(backToListButton.prop('href')).toBe(policyListPathUrlWithPrefix); expect(backToListButton.text()).toBe('Back to policy list'); const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]'); @@ -101,9 +109,9 @@ describe('Policy Details', () => { const backToListButton = policyView.find( 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' ); - expect(history.location.pathname).toEqual('/policy/1'); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); backToListButton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual('/policy'); + expect(history.location.pathname).toEqual(policyListPathUrl); }); it('should display agent stats', async () => { await asyncActions; @@ -130,9 +138,9 @@ describe('Policy Details', () => { const cancelbutton = policyView.find( 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' ); - expect(history.location.pathname).toEqual('/policy/1'); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); cancelbutton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual('/policy'); + expect(history.location.pathname).toEqual(policyListPathUrl); }); it('should display save button', async () => { await asyncActions; diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx index c928a374502a5..bddbd378f9427 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx @@ -28,18 +28,21 @@ import { isLoading, apiError, } from '../store/policy_details/selectors'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; -import { AppAction } from '../../common/store/actions'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { PageView, PageViewHeaderTitle } from '../../common/components/endpoint/page_view'; +import { AppAction } from '../../../../common/store/actions'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { getManagementUrl } from '../../../common/routing'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); - const { notifications, services } = useKibana(); + const { notifications } = useKibana(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -81,7 +84,9 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyName, policyUpdateStatus]); - const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); + const handleBackToListOnClick = useNavigateByRouterEventHandler( + getManagementUrl({ name: 'policyList', excludePrefix: true }) + ); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); @@ -103,7 +108,7 @@ export const PolicyDetails = React.memo(() => { // Else, if we have an error, then show error on the page. if (!policyItem) { return ( - + {isPolicyLoading ? ( ) : policyApiError ? ( @@ -111,7 +116,8 @@ export const PolicyDetails = React.memo(() => { {policyApiError?.message} ) : null} - + + ); } @@ -122,7 +128,7 @@ export const PolicyDetails = React.memo(() => { iconType="arrowLeft" contentProps={{ style: { paddingLeft: '0' } }} onClick={handleBackToListOnClick} - href={`${services.http.basePath.get()}/app/endpoint/policy`} + href={getManagementUrl({ name: 'policyList' })} > { onConfirm={handleSaveConfirmation} /> )} - { - + + ); }); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/config_form.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/config_form.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/config_form.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/config_form.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx similarity index 95% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx index fe062526c8d3c..e5f3b2c7e8b7e 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx @@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { policyConfig } from '../../../store/policy_details/selectors'; import { PolicyDetailsAction } from '../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const EventsCheckbox = React.memo(function ({ name, diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/index.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/index.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/index.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx similarity index 97% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx index ff7296ad5a44e..a4f5bb83b6ef3 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx similarity index 97% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx index 1c6d96e555cef..af28a4803518c 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx similarity index 98% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx index 8add5bed23a29..feddf78cd9c5f 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig, Immutable } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx similarity index 98% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 69c0faf6e800e..e60713ca32d5b 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -11,7 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer, htmlIdGenerator } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Immutable, ProtectionModes } from '../../../../../common/endpoint/types'; +import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts new file mode 100644 index 0000000000000..97436064eebe2 --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector } from 'react-redux'; +import { PolicyListState, PolicyDetailsState } from '../types'; +import { State } from '../../../../common/store'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../../../common/constants'; + +/** + * Narrows global state down to the PolicyListState before calling the provided Policy List Selector + * @param selector + */ +export function usePolicyListSelector(selector: (state: PolicyListState) => TSelected) { + return useSelector((state: State) => { + return selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][ + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE + ] as PolicyListState + ); + }); +} + +/** + * Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector + * @param selector + */ +export function usePolicyDetailsSelector( + selector: (state: PolicyDetailsState) => TSelected +) { + return useSelector((state: State) => + selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][ + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE + ] as PolicyDetailsState + ) + ); +} diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx similarity index 84% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx index a9aea57239ed1..3a8004aa2ec6d 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx @@ -20,11 +20,13 @@ import { } from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { Immutable, PolicyData } from '../../../common/endpoint/types'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { PageView } from '../../common/components/endpoint/page_view'; -import { LinkToApp } from '../../common/components/endpoint/link_to_app'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { Immutable, PolicyData } from '../../../../../common/endpoint/types'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { getManagementUrl } from '../../../common/routing'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -93,14 +95,13 @@ export const PolicyList = React.memo(() => { }), // eslint-disable-next-line react/display-name render: (value: string, item: Immutable) => { - const routeUri = `/policy/${item.id}`; - return ( - - ); + const routePath = getManagementUrl({ + name: 'policyDetails', + policyId: item.id, + excludePrefix: true, + }); + const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id }); + return ; }, truncateText: true, }, @@ -150,7 +151,7 @@ export const PolicyList = React.memo(() => { ); return ( - { onChange={handleTableChange} data-test-subj="policyTable" /> - + + ); }); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts b/x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts similarity index 92% rename from x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts rename to x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts index dd74980add7e0..6a3aecb4a6503 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts @@ -5,7 +5,7 @@ */ import styled from 'styled-components'; -import { EuiTheme } from '../../../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../../../../../../legacy/common/eui_styled_components'; type SpacingOptions = keyof EuiTheme['eui']['spacerSizes']; diff --git a/x-pack/plugins/siem/public/management/routes.tsx b/x-pack/plugins/siem/public/management/routes.tsx new file mode 100644 index 0000000000000..fbcea37c76962 --- /dev/null +++ b/x-pack/plugins/siem/public/management/routes.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Route } from 'react-router-dom'; +import { ManagementContainer } from './pages'; +import { MANAGEMENT_ROUTING_ROOT_PATH } from './common/constants'; + +/** + * Returns the React Router Routes for the management area + */ +export const getManagementRoutes = () => [ + // Mounts the Management interface on `/management` + , +]; diff --git a/x-pack/plugins/siem/public/management/store/index.ts b/x-pack/plugins/siem/public/management/store/index.ts new file mode 100644 index 0000000000000..50049f9828082 --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { managementReducer, getManagementInitialState } from './reducer'; +export { managementMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/management/store/middleware.ts b/x-pack/plugins/siem/public/management/store/middleware.ts new file mode 100644 index 0000000000000..f73736e04a5b7 --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/middleware.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ImmutableMultipleMiddlewareFactory, substateMiddlewareFactory } from '../../common/store'; +import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; +import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; + +// @ts-ignore +export const managementMiddlewareFactory: ImmutableMultipleMiddlewareFactory = ( + coreStart, + depsStart +) => { + return [ + substateMiddlewareFactory( + (globalState) => + globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE], + policyListMiddlewareFactory(coreStart, depsStart) + ), + substateMiddlewareFactory( + (globalState) => + globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE], + policyDetailsMiddlewareFactory(coreStart, depsStart) + ), + ]; +}; diff --git a/x-pack/plugins/siem/public/management/store/reducer.ts b/x-pack/plugins/siem/public/management/store/reducer.ts new file mode 100644 index 0000000000000..ba7927684ad3d --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/reducer.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers as reduxCombineReducers } from 'redux'; +import { + initialPolicyDetailsState, + policyDetailsReducer, +} from '../pages/policy/store/policy_details/reducer'; +import { + initialPolicyListState, + policyListReducer, +} from '../pages/policy/store/policy_list/reducer'; +import { + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; +import { ImmutableCombineReducers } from '../../common/store'; +import { AppAction } from '../../common/store/actions'; +import { ManagementState } from './types'; + +// Change the type of `combinerReducers` locally +const combineReducers: ImmutableCombineReducers = reduxCombineReducers; + +/** + * Returns the initial state of the store for the SIEM Management section + */ +export const getManagementInitialState = (): ManagementState => { + return { + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), + }; +}; + +/** + * Redux store reducer for the SIEM Management section + */ +export const managementReducer = combineReducers({ + // @ts-ignore + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, + // @ts-ignore + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, +}); diff --git a/x-pack/plugins/siem/public/management/store/types.ts b/x-pack/plugins/siem/public/management/store/types.ts new file mode 100644 index 0000000000000..884724982fa8f --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Immutable } from '../../../common/endpoint/types'; +import { PolicyDetailsState, PolicyListState } from '../pages/policy/types'; +import { ImmutableReducer } from '../../common/store'; +import { AppAction } from '../../common/store/actions'; + +/** + * Redux store state for the Management section + */ +export interface ManagementState { + policyDetails: Immutable; + policyList: Immutable; +} + +export interface ManagementPluginState { + management: ManagementState; +} + +export interface ManagementPluginReducer { + management: ImmutableReducer; +} diff --git a/x-pack/plugins/siem/public/management/types.ts b/x-pack/plugins/siem/public/management/types.ts new file mode 100644 index 0000000000000..5ee16bcd434e3 --- /dev/null +++ b/x-pack/plugins/siem/public/management/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SiemPageName } from '../app/types'; + +/** + * The type for the management store global namespace. Used mostly internally to reference + * the type while defining more complex interfaces/types + */ +export type ManagementStoreGlobalNamespace = 'management'; + +/** + * The management list of sub-tabs. Changes to these will impact the Router routes. + */ +export enum ManagementSubTab { + endpoints = 'endpoints', + policies = 'policy', +} + +/** + * The URL route params for the Management Policy List section + */ +export interface ManagementRoutePolicyListParams { + pageName: SiemPageName.management; + tabName: ManagementSubTab.policies; +} + +/** + * The URL route params for the Management Policy Details section + */ +export interface ManagementRoutePolicyDetailsParams extends ManagementRoutePolicyListParams { + policyId: string; +} diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index 9bea776220720..4b8fc078fc016 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -64,13 +64,8 @@ export class Plugin implements IPlugin Date: Fri, 29 May 2020 10:26:00 -0400 Subject: [PATCH 03/69] Refactoring nav links and header components (#66685) Co-authored-by: spalger --- ...n-core-public.chromenavlink.euiicontype.md | 2 +- ...a-plugin-core-public.chromenavlink.href.md | 13 + ...kibana-plugin-core-public.chromenavlink.md | 3 +- ...re-public.chromenavlinkupdateablefields.md | 2 +- ...in-plugins-data-public.querystringinput.md | 2 +- ...na-plugin-plugins-data-public.searchbar.md | 4 +- src/core/public/chrome/chrome_service.tsx | 50 +- src/core/public/chrome/nav_links/nav_link.ts | 14 +- .../public/chrome/nav_links/to_nav_link.ts | 27 +- .../recently_accessed_service.ts | 2 +- .../collapsible_nav.test.tsx.snap | 1010 +- .../header/__snapshots__/header.test.tsx.snap | 14297 ++++++++++++++++ .../header_breadcrumbs.test.tsx.snap | 2 + src/core/public/chrome/ui/header/_index.scss | 2 - ...lapsible_nav.scss => collapsible_nav.scss} | 0 .../chrome/ui/header/collapsible_nav.test.tsx | 98 +- .../chrome/ui/header/collapsible_nav.tsx | 115 +- .../public/chrome/ui/header/header.test.tsx | 106 + src/core/public/chrome/ui/header/header.tsx | 299 +- .../ui/header/header_breadcrumbs.test.tsx | 19 +- .../chrome/ui/header/header_breadcrumbs.tsx | 96 +- .../public/chrome/ui/header/header_logo.tsx | 23 +- .../chrome/ui/header/header_nav_controls.tsx | 39 +- .../public/chrome/ui/header/nav_drawer.tsx | 42 +- src/core/public/chrome/ui/header/nav_link.tsx | 129 +- src/core/public/public.api.md | 3 +- src/core/server/legacy/types.ts | 2 +- src/plugins/data/public/public.api.md | 6 +- 28 files changed, 15657 insertions(+), 750 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md create mode 100644 src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap rename src/core/public/chrome/ui/header/{_collapsible_nav.scss => collapsible_nav.scss} (100%) create mode 100644 src/core/public/chrome/ui/header/header.test.tsx diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md index fe95cb38cd97c..e30e8262f40b2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md @@ -4,7 +4,7 @@ ## ChromeNavLink.euiIconType property -A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. +A EUI iconType that will be used for the app's icon. This icon takes precedence over the `icon` property. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md new file mode 100644 index 0000000000000..a8af0c997ca78 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [href](./kibana-plugin-core-public.chromenavlink.href.md) + +## ChromeNavLink.href property + +Settled state between `url`, `baseUrl`, and `active` + +Signature: + +```typescript +readonly href?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index a9fabb38df869..0349e865bff97 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -20,8 +20,9 @@ export interface ChromeNavLink | [category](./kibana-plugin-core-public.chromenavlink.category.md) | AppCategory | The category the app lives in | | [disabled](./kibana-plugin-core-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | | [disableSubUrlTracking](./kibana-plugin-core-public.chromenavlink.disablesuburltracking.md) | boolean | A flag that tells legacy chrome to ignore the link when tracking sub-urls | -| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | | [hidden](./kibana-plugin-core-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | +| [href](./kibana-plugin-core-public.chromenavlink.href.md) | string | Settled state between url, baseUrl, and active | | [icon](./kibana-plugin-core-public.chromenavlink.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | | [linkToLastSubUrl](./kibana-plugin-core-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md index 7f6dc7e0d5640..bd5a1399cded7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type ChromeNavLinkUpdateableFields = Partial>; +export declare type ChromeNavLinkUpdateableFields = Partial>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index 58690300b3bd6..85eb4825bc2e3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index b015ebfcbaada..fc141b8c89c18 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index fc7e78f209022..67cd43f0647e4 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -36,7 +36,7 @@ import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; -import { Header, LoadingIndicator } from './ui'; +import { Header } from './ui'; import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -214,31 +214,29 @@ export class ChromeService { docTitle, getHeaderComponent: () => ( - - -
- +
), setAppTitle: (appTitle: string) => appTitle$.next(appTitle), diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index fb2972735c2b7..55b5c80526bab 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -62,7 +62,7 @@ export interface ChromeNavLink { /** * A EUI iconType that will be used for the app's icon. This icon - * takes precendence over the `icon` property. + * takes precedence over the `icon` property. */ readonly euiIconType?: string; @@ -72,6 +72,14 @@ export interface ChromeNavLink { */ readonly icon?: string; + /** + * Settled state between `url`, `baseUrl`, and `active` + * + * @internalRemarks + * This should be required once legacy apps are gone. + */ + readonly href?: string; + /** LEGACY FIELDS */ /** @@ -144,7 +152,7 @@ export interface ChromeNavLink { /** @public */ export type ChromeNavLinkUpdateableFields = Partial< - Pick + Pick >; export class NavLinkWrapper { @@ -162,7 +170,7 @@ export class NavLinkWrapper { public update(newProps: ChromeNavLinkUpdateableFields) { // Enforce limited properties at runtime for JS code - newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase']); + newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase', 'href']); return new NavLinkWrapper({ ...this.properties, ...newProps }); } } diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index f79b1df77f8e1..24744fe53c82c 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -24,7 +24,12 @@ import { appendAppPath } from '../../application/utils'; export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; - const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!); + const relativeBaseUrl = isLegacyApp(app) + ? basePath.prepend(app.appUrl) + : basePath.prepend(app.appRoute!); + const url = relativeToAbsolute(appendAppPath(relativeBaseUrl, app.defaultPath)); + const baseUrl = relativeToAbsolute(relativeBaseUrl); + return new NavLinkWrapper({ ...app, hidden: useAppStatus @@ -32,17 +37,27 @@ export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWra : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, legacy: isLegacyApp(app), - baseUrl: relativeToAbsolute(baseUrl), + baseUrl, ...(isLegacyApp(app) - ? {} + ? { + href: url && !url.startsWith(app.subUrlBase!) ? url : baseUrl, + } : { - url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)), + href: url, + url, }), }); } -function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls +/** + * @param {string} url - a relative or root relative url. If a relative path is given then the + * absolute url returned will depend on the current page where this function is called from. For example + * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get + * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that + * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". + * @return {string} the relative url transformed into an absolute url + */ +export function relativeToAbsolute(url: string) { const a = document.createElement('a'); a.setAttribute('href', url); return a.href; diff --git a/src/core/public/chrome/recently_accessed/recently_accessed_service.ts b/src/core/public/chrome/recently_accessed/recently_accessed_service.ts index 27dbc288d18cb..86c7f3a1ef765 100644 --- a/src/core/public/chrome/recently_accessed/recently_accessed_service.ts +++ b/src/core/public/chrome/recently_accessed/recently_accessed_service.ts @@ -76,7 +76,7 @@ export interface ChromeRecentlyAccessed { * * @param link a relative URL to the resource (not including the {@link HttpStart.basePath | `http.basePath`}) * @param label the label to display in the UI - * @param id a unique string used to de-duplicate the recently accessed llist. + * @param id a unique string used to de-duplicate the recently accessed list. */ add(link: string, label: string, id: string): void; diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 866ea5f45d986..f5b17f8d214e9 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -2,140 +2,295 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
@@ -376,7 +531,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-label="recent 2" class="euiListGroupItem__button" data-test-subj="collapsibleNavAppLink--recent" - href="recent 2" + href="http://localhost/recent%202" rel="noreferrer" title="recent 2" > @@ -465,7 +620,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` style="max-width: none;" >
  • @@ -1023,7 +1179,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-label="recent 2" class="euiListGroupItem__button" data-test-subj="collapsibleNavAppLink--recent" - href="recent 2" + href="http://localhost/recent%202" rel="noreferrer" title="recent 2" > @@ -1112,7 +1268,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` style="max-width: none;" >
  • + +
    +
    +
    + +
  • +`; + +exports[`Header renders 2`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + +
    +
    +`; + +exports[`Header renders 3`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + + + +
    + + + + +
    + + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + +
    + +
    +
    +
    + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
    +
    + +
    + + + + + +
    +
    +`; + +exports[`Header renders 4`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + + +
    + + + +
    +
    +
    + +
    + + + + +
    + + + + +
    + + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + +
    +
    +`; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap index d089019915686..fdaa17c279a10 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -5,6 +5,7 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = aria-current="page" className="euiBreadcrumb euiBreadcrumb--last" data-test-subj="breadcrumb first last" + title="First" > First @@ -39,6 +40,7 @@ Array [ aria-current="page" className="euiBreadcrumb euiBreadcrumb--last" data-test-subj="breadcrumb last" + title="Second" > Second , diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 1b0438d748ff0..5c5e7f18b60a4 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,5 +1,3 @@ -@import './collapsible_nav'; - // TODO #64541 // Delete this block .chrHeaderWrapper:not(.headerWrapper) { diff --git a/src/core/public/chrome/ui/header/_collapsible_nav.scss b/src/core/public/chrome/ui/header/collapsible_nav.scss similarity index 100% rename from src/core/public/chrome/ui/header/_collapsible_nav.scss rename to src/core/public/chrome/ui/header/collapsible_nav.scss diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 527f0df598c7c..5a734d55445a2 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -19,11 +19,13 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; -import { CollapsibleNav } from './collapsible_nav'; -import { DEFAULT_APP_CATEGORIES } from '../../..'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { NavLink, RecentNavLink } from './nav_link'; +import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; +import { CollapsibleNav } from './collapsible_nav'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -31,40 +33,42 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; -function mockLink({ label = 'discover', category, onClick }: Partial) { +function mockLink({ title = 'discover', category }: Partial) { return { - key: label, - label, - href: label, - isActive: true, - onClick: onClick || (() => {}), + title, category, - 'data-test-subj': label, + id: title, + href: title, + baseUrl: '/', + legacy: false, + isActive: true, + 'data-test-subj': title, }; } -function mockRecentNavLink({ label = 'recent', onClick }: Partial) { +function mockRecentNavLink({ label = 'recent' }: Partial) { return { - href: label, label, - title: label, - 'aria-label': label, - onClick, + link: label, + id: label, }; } function mockProps() { return { - id: 'collapsible-nav', - homeHref: '/', + appId$: new BehaviorSubject('test'), + basePath: httpServiceMock.createSetupContract({ basePath: '/test' }).basePath, + id: 'collapsibe-nav', isLocked: false, isOpen: false, - navLinks: [], - recentNavLinks: [], + homeHref: '/', + legacyMode: false, + navLinks$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), storage: new StubBrowserStorage(), - onIsOpenUpdate: () => {}, onIsLockedUpdate: () => {}, - navigateToApp: () => {}, + closeNav: () => {}, + navigateToApp: () => Promise.resolve(), }; } @@ -103,14 +107,14 @@ describe('CollapsibleNav', () => { it('renders links grouped by category', () => { // just a test of category functionality, categories are not accurate const navLinks = [ - mockLink({ label: 'discover', category: kibana }), - mockLink({ label: 'siem', category: security }), - mockLink({ label: 'metrics', category: observability }), - mockLink({ label: 'monitoring', category: management }), - mockLink({ label: 'visualize', category: kibana }), - mockLink({ label: 'dashboard', category: kibana }), - mockLink({ label: 'canvas' }), // links should be able to be rendered top level as well - mockLink({ label: 'logs', category: observability }), + mockLink({ title: 'discover', category: kibana }), + mockLink({ title: 'siem', category: security }), + mockLink({ title: 'metrics', category: observability }), + mockLink({ title: 'monitoring', category: management }), + mockLink({ title: 'visualize', category: kibana }), + mockLink({ title: 'dashboard', category: kibana }), + mockLink({ title: 'canvas' }), // links should be able to be rendered top level as well + mockLink({ title: 'logs', category: observability }), ]; const recentNavLinks = [ mockRecentNavLink({ label: 'recent 1' }), @@ -120,8 +124,8 @@ describe('CollapsibleNav', () => { ); expect(component).toMatchSnapshot(); @@ -134,8 +138,8 @@ describe('CollapsibleNav', () => { ); expectShownNavLinksCount(component, 3); @@ -149,32 +153,34 @@ describe('CollapsibleNav', () => { }); it('closes the nav after clicking a link', () => { - const onClick = sinon.spy(); - const onIsOpenUpdate = sinon.spy(); - const navLinks = [mockLink({ category: kibana, onClick })]; - const recentNavLinks = [mockRecentNavLink({ onClick })]; + const onClose = sinon.spy(); + const navLinks = [mockLink({ category: kibana }), mockLink({ title: 'categoryless' })]; + const recentNavLinks = [mockRecentNavLink({})]; const component = mount( ); component.setProps({ - onIsOpenUpdate: (isOpen: boolean) => { - component.setProps({ isOpen }); - onIsOpenUpdate(); + closeNav: () => { + component.setProps({ isOpen: false }); + onClose(); }, }); component.find('[data-test-subj="collapsibleNavGroup-recentlyViewed"] a').simulate('click'); - expect(onClick.callCount).toEqual(1); - expect(onIsOpenUpdate.callCount).toEqual(1); + expect(onClose.callCount).toEqual(1); expectNavIsClosed(component); component.setProps({ isOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-kibana"] a').simulate('click'); - expect(onClick.callCount).toEqual(2); - expect(onIsOpenUpdate.callCount).toEqual(2); + expect(onClose.callCount).toEqual(2); + expectNavIsClosed(component); + component.setProps({ isOpen: true }); + component.find('[data-test-subj="collapsibleNavGroup-noCategory"] a').simulate('click'); + expect(onClose.callCount).toEqual(3); + expectNavIsClosed(component); }); }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 8bca42db23517..9494e22920de8 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -17,6 +17,7 @@ * under the License. */ +import './collapsible_nav.scss'; import { EuiCollapsibleNav, EuiCollapsibleNavGroup, @@ -30,11 +31,16 @@ import { import { i18n } from '@kbn/i18n'; import { groupBy, sortBy } from 'lodash'; import React, { useRef } from 'react'; +import { useObservable } from 'react-use'; +import * as Rx from 'rxjs'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { NavLink, RecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink } from './nav_link'; -function getAllCategories(allCategorizedLinks: Record) { +function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; for (const [key, value] of Object.entries(allCategorizedLinks)) { @@ -45,7 +51,7 @@ function getAllCategories(allCategorizedLinks: Record) { } function getOrderedCategories( - mainCategories: Record, + mainCategories: Record, categoryDictionary: ReturnType ) { return sortBy( @@ -69,35 +75,53 @@ function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { } interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; + id: string; isLocked: boolean; isOpen: boolean; - navLinks: NavLink[]; - recentNavLinks: RecentNavLink[]; homeHref: string; - id: string; + legacyMode: boolean; + navLinks$: Rx.Observable; + recentlyAccessed$: Rx.Observable; storage?: Storage; onIsLockedUpdate: OnIsLockedUpdate; - onIsOpenUpdate: (isOpen?: boolean) => void; - navigateToApp: (appId: string) => void; + closeNav: () => void; + navigateToApp: InternalApplicationStart['navigateToApp']; } export function CollapsibleNav({ + basePath, + id, isLocked, isOpen, - navLinks, - recentNavLinks, - onIsLockedUpdate, - onIsOpenUpdate, homeHref, - id, - navigateToApp, + legacyMode, storage = window.localStorage, + onIsLockedUpdate, + closeNav, + navigateToApp, + ...observables }: Props) { + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); + const appId = useObservable(observables.appId$, ''); const lockRef = useRef(null); const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); + const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => { + return createEuiListItem({ + link, + legacyMode, + appId, + dataTestSubj: 'collapsibleNavAppLink', + navigateToApp, + onClick: closeNav, + ...(needsIcon && { basePath }), + }); + }; return ( {/* Pinned items */} @@ -127,7 +151,7 @@ export function CollapsibleNav({ iconType: 'home', href: homeHref, onClick: (event: React.MouseEvent) => { - onIsOpenUpdate(false); + closeNav(); if ( event.isDefaultPrevented() || event.altKey || @@ -159,21 +183,22 @@ export function CollapsibleNav({ onToggle={(isCategoryOpen) => setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} data-test-subj="collapsibleNavGroup-recentlyViewed" > - {recentNavLinks.length > 0 ? ( + {recentlyAccessed.length > 0 ? ( {}, ...link }) => ({ - 'data-test-subj': 'collapsibleNavAppLink--recent', - onClick: (e: React.MouseEvent) => { - onIsOpenUpdate(false); - onClick(e); - }, - ...link, - }))} + listItems={recentlyAccessed.map((link) => { + // TODO #64541 + // Can remove icon from recent links completely + const { iconType, ...hydratedLink } = createRecentNavLink(link, navLinks, basePath); + + return { + ...hydratedLink, + 'data-test-subj': 'collapsibleNavAppLink--recent', + onClick: closeNav, + }; + })} maxWidth="none" color="subdued" gutterSize="none" @@ -195,21 +220,8 @@ export function CollapsibleNav({ {/* Kibana, Observability, Security, and Management sections */} - {orderedCategories.map((categoryName, i) => { + {orderedCategories.map((categoryName) => { const category = categoryDictionary[categoryName]!; - const links = allCategorizedLinks[categoryName].map( - ({ label, href, isActive, isDisabled, onClick }) => ({ - label, - href, - isActive, - isDisabled, - 'data-test-subj': 'collapsibleNavAppLink', - onClick: (e: React.MouseEvent) => { - onIsOpenUpdate(false); - onClick(e); - }, - }) - ); return ( readyForEUI(link))} maxWidth="none" color="subdued" gutterSize="none" @@ -237,23 +249,10 @@ export function CollapsibleNav({ })} {/* Things with no category (largely for custom plugins) */} - {unknowns.map(({ label, href, icon, isActive, isDisabled, onClick }, i) => ( - + {unknowns.map((link, i) => ( + - ) => { - onIsOpenUpdate(false); - onClick(e); - }} - /> + ))} diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx new file mode 100644 index 0000000000000..13e1f6f086ae2 --- /dev/null +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { NavType } from '.'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { applicationServiceMock } from '../../../mocks'; +import { Header } from './header'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +function mockProps() { + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + const application = applicationServiceMock.createInternalStartContract(); + + return { + application, + kibanaVersion: '1.0.0', + appTitle$: new BehaviorSubject('test'), + badge$: new BehaviorSubject(undefined), + breadcrumbs$: new BehaviorSubject([]), + homeHref: '/', + isVisible$: new BehaviorSubject(true), + kibanaDocLink: '/docs', + navLinks$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), + forceAppSwitcherNavigation$: new BehaviorSubject(false), + helpExtension$: new BehaviorSubject(undefined), + helpSupportUrl$: new BehaviorSubject(''), + legacyMode: false, + navControlsLeft$: new BehaviorSubject([]), + navControlsRight$: new BehaviorSubject([]), + basePath: http.basePath, + isLocked$: new BehaviorSubject(false), + navType$: new BehaviorSubject('modern' as NavType), + loadingCount$: new BehaviorSubject(0), + onIsLockedUpdate: () => {}, + }; +} + +describe('Header', () => { + beforeAll(() => { + Object.defineProperty(window, 'localStorage', { + value: new StubBrowserStorage(), + }); + }); + + it('renders', () => { + const isVisible$ = new BehaviorSubject(false); + const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); + const isLocked$ = new BehaviorSubject(false); + const navType$ = new BehaviorSubject('modern' as NavType); + const navLinks$ = new BehaviorSubject([ + { id: 'kibana', title: 'kibana', baseUrl: '', legacy: false }, + ]); + const recentlyAccessed$ = new BehaviorSubject([ + { link: '', label: 'dashboard', id: 'dashboard' }, + ]); + const component = mountWithIntl( +
    + ); + expect(component).toMatchSnapshot(); + + act(() => isVisible$.next(true)); + component.update(); + expect(component).toMatchSnapshot(); + + act(() => isLocked$.next(true)); + component.update(); + expect(component).toMatchSnapshot(); + + act(() => navType$.next('legacy' as NavType)); + component.update(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 6280d68587355..d24b342e0386b 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -28,9 +28,11 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Component, createRef } from 'react'; import classnames from 'classnames'; -import * as Rx from 'rxjs'; +import React, { createRef, useState } from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import { LoadingIndicator } from '../'; import { ChromeBadge, ChromeBreadcrumb, @@ -41,192 +43,101 @@ import { import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; -import { HeaderBadge } from './header_badge'; import { NavType, OnIsLockedUpdate } from './'; +import { CollapsibleNav } from './collapsible_nav'; +import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; -import { HeaderNavControls } from './header_nav_controls'; -import { createNavLink, createRecentNavLink } from './nav_link'; import { HeaderLogo } from './header_logo'; +import { HeaderNavControls } from './header_nav_controls'; import { NavDrawer } from './nav_drawer'; -import { CollapsibleNav } from './collapsible_nav'; export interface HeaderProps { kibanaVersion: string; application: InternalApplicationStart; - appTitle$: Rx.Observable; - badge$: Rx.Observable; - breadcrumbs$: Rx.Observable; + appTitle$: Observable; + badge$: Observable; + breadcrumbs$: Observable; homeHref: string; - isVisible$: Rx.Observable; + isVisible$: Observable; kibanaDocLink: string; - navLinks$: Rx.Observable; - recentlyAccessed$: Rx.Observable; - forceAppSwitcherNavigation$: Rx.Observable; - helpExtension$: Rx.Observable; - helpSupportUrl$: Rx.Observable; + navLinks$: Observable; + recentlyAccessed$: Observable; + forceAppSwitcherNavigation$: Observable; + helpExtension$: Observable; + helpSupportUrl$: Observable; legacyMode: boolean; - navControlsLeft$: Rx.Observable; - navControlsRight$: Rx.Observable; + navControlsLeft$: Observable; + navControlsRight$: Observable; basePath: HttpStart['basePath']; - isLocked$: Rx.Observable; - navType$: Rx.Observable; + isLocked$: Observable; + navType$: Observable; + loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; } -interface State { - appTitle: string; - isVisible: boolean; - navLinks: ChromeNavLink[]; - recentlyAccessed: ChromeRecentlyAccessedHistoryItem[]; - forceNavigation: boolean; - navControlsLeft: readonly ChromeNavControl[]; - navControlsRight: readonly ChromeNavControl[]; - currentAppId: string | undefined; - isLocked: boolean; - navType: NavType; - isOpen: boolean; +function renderMenuTrigger(toggleOpen: () => void) { + return ( + + + + ); } -export class Header extends Component { - private subscription?: Rx.Subscription; - private navDrawerRef = createRef(); - private toggleCollapsibleNavRef = createRef(); - - constructor(props: HeaderProps) { - super(props); - - let isLocked = false; - props.isLocked$.subscribe((initialIsLocked) => (isLocked = initialIsLocked)); - - this.state = { - appTitle: 'Kibana', - isVisible: true, - navLinks: [], - recentlyAccessed: [], - forceNavigation: false, - navControlsLeft: [], - navControlsRight: [], - currentAppId: '', - isLocked, - navType: 'modern', - isOpen: false, - }; +export function Header({ + kibanaVersion, + kibanaDocLink, + legacyMode, + application, + basePath, + onIsLockedUpdate, + homeHref, + ...observables +}: HeaderProps) { + const isVisible = useObservable(observables.isVisible$, true); + const navType = useObservable(observables.navType$, 'modern'); + const isLocked = useObservable(observables.isLocked$, false); + const [isOpen, setIsOpen] = useState(false); + + if (!isVisible) { + return ; } - public componentDidMount() { - this.subscription = Rx.combineLatest( - this.props.appTitle$, - this.props.isVisible$, - this.props.forceAppSwitcherNavigation$, - this.props.navLinks$, - this.props.recentlyAccessed$, - // Types for combineLatest only handle up to 6 inferred types so we combine these separately. - Rx.combineLatest( - this.props.navControlsLeft$, - this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.isLocked$, - this.props.navType$ - ) - ).subscribe({ - next: ([ - appTitle, - isVisible, - forceNavigation, - navLinks, - recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, isLocked, navType], - ]) => { - this.setState({ - appTitle, - isVisible, - forceNavigation, - navLinks: navLinks.filter((navLink) => !navLink.hidden), - recentlyAccessed, - navControlsLeft, - navControlsRight, - currentAppId, - isLocked, - navType, - }); - }, - }); - } - - public componentWillUnmount() { - if (this.subscription) { - this.subscription.unsubscribe(); + const navDrawerRef = createRef(); + const toggleCollapsibleNavRef = createRef(); + const navId = htmlIdGenerator()(); + const className = classnames( + 'chrHeaderWrapper', // TODO #64541 - delete this + 'hide-for-sharing', + { + 'chrHeaderWrapper--navIsLocked': isLocked, + headerWrapper: navType === 'modern', } - } - - public renderMenuTrigger() { - return ( - this.navDrawerRef.current?.toggleOpen()} - > - - - ); - } - - public render() { - const { appTitle, isVisible, navControlsLeft, navControlsRight } = this.state; - const { - badge$, - breadcrumbs$, - helpExtension$, - helpSupportUrl$, - kibanaDocLink, - kibanaVersion, - } = this.props; - const navLinks = this.state.navLinks.map((link) => - createNavLink( - link, - this.props.legacyMode, - this.state.currentAppId, - this.props.basePath, - this.props.application.navigateToApp - ) - ); - const recentNavLinks = this.state.recentlyAccessed.map((link) => - createRecentNavLink(link, this.state.navLinks, this.props.basePath) - ); + ); - if (!isVisible) { - return null; - } - - const className = classnames( - 'chrHeaderWrapper', // TODO #64541 - delete this - 'hide-for-sharing', - { - 'chrHeaderWrapper--navIsLocked': this.state.isLocked, - headerWrapper: this.state.navType === 'modern', - } - ); - const navId = htmlIdGenerator()(); - return ( + return ( + <> +
    - {this.state.navType === 'modern' ? ( + {navType === 'modern' ? ( { - this.setState({ isOpen: !this.state.isOpen }); - }} - aria-expanded={this.state.isOpen} - aria-pressed={this.state.isOpen} + onClick={() => setIsOpen(!isOpen)} + aria-expanded={isOpen} + aria-pressed={isOpen} aria-controls={navId} - ref={this.toggleCollapsibleNavRef} + ref={toggleCollapsibleNavRef} > @@ -236,71 +147,79 @@ export class Header extends Component { // Delete this block - {this.renderMenuTrigger()} + {renderMenuTrigger(() => navDrawerRef.current?.toggleOpen())} )} - + - + - + - + - {this.state.navType === 'modern' ? ( + {navType === 'modern' ? ( { - this.setState({ isOpen }); - if (this.toggleCollapsibleNavRef.current) { - this.toggleCollapsibleNavRef.current.focus(); + isLocked={isLocked} + navLinks$={observables.navLinks$} + recentlyAccessed$={observables.recentlyAccessed$} + isOpen={isOpen} + homeHref={homeHref} + basePath={basePath} + legacyMode={legacyMode} + navigateToApp={application.navigateToApp} + onIsLockedUpdate={onIsLockedUpdate} + closeNav={() => { + setIsOpen(false); + if (toggleCollapsibleNavRef.current) { + toggleCollapsibleNavRef.current.focus(); } }} - navigateToApp={this.props.application.navigateToApp} /> ) : ( // TODO #64541 // Delete this block )}
    - ); - } + + ); } diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 0398f162f9af9..7fe2c91087090 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -19,26 +19,23 @@ import { mount } from 'enzyme'; import React from 'react'; -import * as Rx from 'rxjs'; - -import { ChromeBreadcrumb } from '../../chrome_service'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { - const breadcrumbs$ = new Rx.Subject(); - const wrapper = mount(); - - breadcrumbs$.next([{ text: 'First' }]); - // Unfortunately, enzyme won't update the wrapper until we call update. - wrapper.update(); + const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); + const wrapper = mount( + + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }]); + act(() => breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }])); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - breadcrumbs$.next([]); + act(() => breadcrumbs$.next([])); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 54cfc7131cb2b..174c46981db53 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -17,88 +17,36 @@ * under the License. */ -import classNames from 'classnames'; -import React, { Component } from 'react'; -import * as Rx from 'rxjs'; - import { EuiHeaderBreadcrumbs } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; import { ChromeBreadcrumb } from '../../chrome_service'; interface Props { - appTitle?: string; - breadcrumbs$: Rx.Observable; + appTitle$: Observable; + breadcrumbs$: Observable; } -interface State { - breadcrumbs: ChromeBreadcrumb[]; -} - -export class HeaderBreadcrumbs extends Component { - private subscription?: Rx.Subscription; - - constructor(props: Props) { - super(props); +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { + const appTitle = useObservable(appTitle$, 'Kibana'); + const breadcrumbs = useObservable(breadcrumbs$, []); + let crumbs = breadcrumbs; - this.state = { breadcrumbs: [] }; + if (breadcrumbs.length === 0 && appTitle) { + crumbs = [{ text: appTitle }]; } - public componentDidMount() { - this.subscribe(); - } - - public componentDidUpdate(prevProps: Props) { - if (prevProps.breadcrumbs$ === this.props.breadcrumbs$) { - return; - } + crumbs = crumbs.map((breadcrumb, i) => ({ + ...breadcrumb, + 'data-test-subj': classNames( + 'breadcrumb', + breadcrumb['data-test-subj'], + i === 0 && 'first', + i === breadcrumbs.length - 1 && 'last' + ), + })); - this.unsubscribe(); - this.subscribe(); - } - - public componentWillUnmount() { - this.unsubscribe(); - } - - public render() { - return ( - - ); - } - - private subscribe() { - this.subscription = this.props.breadcrumbs$.subscribe((breadcrumbs) => { - this.setState({ - breadcrumbs, - }); - }); - } - - private unsubscribe() { - if (this.subscription) { - this.subscription.unsubscribe(); - delete this.subscription; - } - } - - private getBreadcrumbs() { - let breadcrumbs = this.state.breadcrumbs; - - if (breadcrumbs.length === 0 && this.props.appTitle) { - breadcrumbs = [{ text: this.props.appTitle }]; - } - - return breadcrumbs.map((breadcrumb, i) => ({ - ...breadcrumb, - 'data-test-subj': classNames( - 'breadcrumb', - breadcrumb['data-test-subj'], - i === 0 && 'first', - i === breadcrumbs.length - 1 && 'last' - ), - })); - } + return ; } diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 147c7cf5dc4b1..9bec946b6b76e 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -17,11 +17,13 @@ * under the License. */ -import Url from 'url'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiHeaderLogo } from '@elastic/eui'; -import { NavLink } from './nav_link'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import Url from 'url'; +import { ChromeNavLink } from '../..'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -41,7 +43,7 @@ function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { function onClick( event: React.MouseEvent, forceNavigation: boolean, - navLinks: NavLink[], + navLinks: ChromeNavLink[], navigateToApp: (appId: string) => void ) { const anchor = findClosestAnchor((event as any).nativeEvent.target); @@ -50,7 +52,7 @@ function onClick( } const navLink = navLinks.find((item) => item.href === anchor.href); - if (navLink && navLink.isDisabled) { + if (navLink && navLink.disabled) { event.preventDefault(); return; } @@ -85,12 +87,15 @@ function onClick( interface Props { href: string; - navLinks: NavLink[]; - forceNavigation: boolean; + navLinks$: Observable; + forceNavigation$: Observable; navigateToApp: (appId: string) => void; } -export function HeaderLogo({ href, forceNavigation, navLinks, navigateToApp }: Props) { +export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { + const forceNavigation = useObservable(observables.forceNavigation$, false); + const navLinks = useObservable(observables.navLinks$, []); + return ( ; side: 'left' | 'right'; } -export class HeaderNavControls extends Component { - public render() { - const { navControls } = this.props; - - if (!navControls) { - return null; - } +export function HeaderNavControls({ navControls$, side }: Props) { + const navControls = useObservable(navControls$, []); - return navControls.map(this.renderNavControl); + if (!navControls) { + return null; } // It should be performant to use the index as the key since these are unlikely // to change while Kibana is running. - private renderNavControl = (navControl: ChromeNavControl, index: number) => ( - - - + return ( + <> + {navControls.map((navControl: ChromeNavControl, index: number) => ( + + + + ))} + ); } diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index 17df8569f6307..ee4bff6cc0ac4 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -17,24 +17,39 @@ * under the License. */ -import React from 'react'; +import { EuiHorizontalRule, EuiNavDrawer, EuiNavDrawerGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { NavLink, RecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink } from './nav_link'; import { RecentLinks } from './recent_links'; export interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; isLocked?: boolean; + legacyMode: boolean; + navLinks$: Observable; + recentlyAccessed$: Observable; + navigateToApp: CoreStart['application']['navigateToApp']; onIsLockedUpdate?: OnIsLockedUpdate; - navLinks: NavLink[]; - recentNavLinks: RecentNavLink[]; } -function navDrawerRenderer( - { isLocked, onIsLockedUpdate, navLinks, recentNavLinks }: Props, +function NavDrawerRenderer( + { isLocked, onIsLockedUpdate, basePath, legacyMode, navigateToApp, ...observables }: Props, ref: React.Ref ) { + const appId = useObservable(observables.appId$, ''); + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentNavLinks = useObservable(observables.recentlyAccessed$, []).map((link) => + createRecentNavLink(link, navLinks, basePath) + ); + return ( + createEuiListItem({ + link, + legacyMode, + appId, + basePath, + navigateToApp, + dataTestSubj: 'navDrawerAppsMenuLink', + }) + )} aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', { defaultMessage: 'Primary navigation links', })} @@ -58,4 +82,4 @@ function navDrawerRenderer( ); } -export const NavDrawer = React.forwardRef(navDrawerRenderer); +export const NavDrawer = React.forwardRef(NavDrawerRenderer); diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c979bb8271e1b..c09b15fac9bdb 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiImage } from '@elastic/eui'; -import { AppCategory } from 'src/core/types'; -import { ChromeNavLink, CoreStart, ChromeRecentlyAccessedHistoryItem } from '../../../'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; import { HttpStart } from '../../../http'; +import { relativeToAbsolute } from '../../nav_links/to_nav_link'; function isModifiedEvent(event: React.MouseEvent) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); @@ -32,62 +32,37 @@ function LinkIcon({ url }: { url: string }) { return ; } -export interface NavLink { - key: string; - label: string; - href: string; - isActive: boolean; - onClick(event: React.MouseEvent): void; - category?: AppCategory; - isDisabled?: boolean; - iconType?: string; - icon?: JSX.Element; - order?: number; - 'data-test-subj': string; +interface Props { + link: ChromeNavLink; + legacyMode: boolean; + appId: string | undefined; + basePath?: HttpStart['basePath']; + dataTestSubj: string; + onClick?: Function; + navigateToApp: CoreStart['application']['navigateToApp']; } -/** - * Create a link that's actually ready to be passed into EUI - * - * @param navLink - * @param legacyMode - * @param currentAppId - * @param basePath - * @param navigateToApp - */ -export function createNavLink( - navLink: ChromeNavLink, - legacyMode: boolean, - currentAppId: string | undefined, - basePath: HttpStart['basePath'], - navigateToApp: CoreStart['application']['navigateToApp'] -): NavLink { - const { - legacy, - url, - active, - baseUrl, - id, - title, - disabled, - euiIconType, - icon, - category, - order, - tooltip, - } = navLink; - let href = navLink.url ?? navLink.baseUrl; - - if (legacy) { - href = url && !active ? url : baseUrl; - } +// TODO #64541 +// Set return type to EuiListGroupItemProps +// Currently it's a subset of EuiListGroupItemProps+FlyoutMenuItem for CollapsibleNav and NavDrawer +// But FlyoutMenuItem isn't exported from EUI +export function createEuiListItem({ + link, + legacyMode, + appId, + basePath, + onClick = () => {}, + navigateToApp, + dataTestSubj, +}: Props) { + const { legacy, active, id, title, disabled, euiIconType, icon, tooltip, href } = link; return { - category, - key: id, label: tooltip ?? title, - href, // Use href and onClick to support "open in new tab" and SPA navigation in the same link - onClick(event) { + href, + /* Use href and onClick to support "open in new tab" and SPA navigation in the same link */ + onClick(event: React.MouseEvent) { + onClick(); if ( !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps @@ -96,57 +71,31 @@ export function createNavLink( !isModifiedEvent(event) // ignore clicks with modifier keys ) { event.preventDefault(); - navigateToApp(navLink.id); + navigateToApp(id); } }, // Legacy apps use `active` property, NP apps should match the current app - isActive: active || currentAppId === id, + isActive: active || appId === id, isDisabled: disabled, - iconType: euiIconType, - icon: !euiIconType && icon ? : undefined, - order, - 'data-test-subj': 'navDrawerAppsMenuLink', + 'data-test-subj': dataTestSubj, + ...(basePath && { + iconType: euiIconType, + icon: !euiIconType && icon ? : undefined, + }), }; } -// Providing a buffer between the limit and the cut off index -// protects from truncating just the last couple (6) characters -const TRUNCATE_LIMIT: number = 64; -const TRUNCATE_AT: number = 58; - -function truncateRecentItemLabel(label: string): string { - if (label.length > TRUNCATE_LIMIT) { - label = `${label.substring(0, TRUNCATE_AT)}…`; - } - - return label; -} - -/** - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -function relativeToAbsolute(url: string) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - export interface RecentNavLink { href: string; label: string; title: string; 'aria-label': string; iconType?: string; - onClick?(event: React.MouseEvent): void; } /** * Add saved object type info to recently links + * TODO #64541 - set return type to EuiListGroupItemProps * * Recent nav links are similar to normal nav links but are missing some Kibana Platform magic and * because of legacy reasons have slightly different properties. @@ -176,7 +125,7 @@ export function createRecentNavLink( return { href, - label: truncateRecentItemLabel(label), + label, title: titleAndAriaLabel, 'aria-label': titleAndAriaLabel, iconType: navLink?.euiIconType, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4ccded9b9afec..90c5dbb5f6558 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -286,6 +286,7 @@ export interface ChromeNavLink { readonly disableSubUrlTracking?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; + readonly href?: string; readonly icon?: string; readonly id: string; // @internal @@ -314,7 +315,7 @@ export interface ChromeNavLinks { } // @public (undocumented) -export type ChromeNavLinkUpdateableFields = Partial>; +export type ChromeNavLinkUpdateableFields = Partial>; // @public export interface ChromeRecentlyAccessed { diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 2567ca790e04f..98f8d874c7088 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -151,7 +151,7 @@ export type LegacyAppSpec = Partial & { * @internal * @deprecated */ -export type LegacyNavLink = Omit & { +export type LegacyNavLink = Omit & { order: number; }; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 32abfd2694f16..142ec9c8c877e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1366,7 +1366,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -1578,8 +1578,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts From 8f6bef10126fa878411ad3e05626aff75437f1ee Mon Sep 17 00:00:00 2001 From: Eric Beahan Date: Fri, 29 May 2020 09:59:58 -0500 Subject: [PATCH 04/69] Update table of contents to reflect current content (#66835) --- CONTRIBUTING.md | 56 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1053cc2f65396..4bf659345d387 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,26 +13,44 @@ A high level overview of our contributing guidelines. - ["My issue isn't getting enough attention"](#my-issue-isnt-getting-enough-attention) - ["I want to help!"](#i-want-to-help) - [How We Use Git and GitHub](#how-we-use-git-and-github) + - [Forking](#forking) - [Branching](#branching) - [Commits and Merging](#commits-and-merging) + - [Rebasing and fixing merge conflicts](#rebasing-and-fixing-merge-conflicts) - [What Goes Into a Pull Request](#what-goes-into-a-pull-request) - [Contributing Code](#contributing-code) - [Setting Up Your Development Environment](#setting-up-your-development-environment) + - [Increase node.js heap size](#increase-nodejs-heap-size) + - [Running Elasticsearch Locally](#running-elasticsearch-locally) + - [Nightly snapshot (recommended)](#nightly-snapshot-recommended) + - [Keeping data between snapshots](#keeping-data-between-snapshots) + - [Source](#source) + - [Archive](#archive) + - [Sample Data](#sample-data) + - [Running Elasticsearch Remotely](#running-elasticsearch-remotely) + - [Running remote clusters](#running-remote-clusters) + - [Running Kibana](#running-kibana) + - [Running Kibana in Open-Source mode](#running-kibana-in-open-source-mode) + - [Unsupported URL Type](#unsupported-url-type) - [Customizing `config/kibana.dev.yml`](#customizing-configkibanadevyml) + - [Potential Optimization Pitfalls](#potential-optimization-pitfalls) - [Setting Up SSL](#setting-up-ssl) - [Linting](#linting) + - [Setup Guide for VS Code Users](#setup-guide-for-vs-code-users) - [Internationalization](#internationalization) - [Localization](#localization) + - [Styling with SASS](#styling-with-sass) - [Testing and Building](#testing-and-building) - [Debugging server code](#debugging-server-code) - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) - - [Debugging Unit Tests](#debugging-unit-tests) - - [Unit Testing Plugins](#unit-testing-plugins) - - [Automated Accessibility Testing](#automated-accessibility-testing) - - [Cross-browser compatibility](#cross-browser-compatibility) - - [Testing compatibility locally](#testing-compatibility-locally) - - [Running Browser Automation Tests](#running-browser-automation-tests) - - [Browser Automation Notes](#browser-automation-notes) + - [Unit testing frameworks](#unit-testing-frameworks) + - [Running specific Kibana tests](#running-specific-kibana-tests) + - [Debugging Unit Tests](#debugging-unit-tests) + - [Unit Testing Plugins](#unit-testing-plugins) + - [Automated Accessibility Testing](#automated-accessibility-testing) + - [Cross-browser compatibility](#cross-browser-compatibility) + - [Testing compatibility locally](#testing-compatibility-locally) + - [Running Browser Automation Tests](#running-browser-automation-tests) - [Building OS packages](#building-os-packages) - [Writing documentation](#writing-documentation) - [Release Notes Process](#release-notes-process) @@ -414,7 +432,7 @@ extract them to a `JSON` file or integrate translations back to Kibana. To know We cannot support accepting contributions to the translations from any source other than the translators we have engaged to do the work. We are still to develop a proper process to accept any contributed translations. We certainly appreciate that people care enough about the localization effort to want to help improve the quality. We aim to build out a more comprehensive localization process for the future and will notify you once contributions can be supported, but for the time being, we are not able to incorporate suggestions. -### Syling with SASS +### Styling with SASS When writing a new component, create a sibling SASS file of the same name and import directly into the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). @@ -467,10 +485,10 @@ macOS users on a machine with a discrete graphics card may see significant speed - Uncheck the "Prefer integrated to discrete GPU" option - Restart iTerm -### Debugging Server Code +#### Debugging Server Code `yarn debug` will start the server with Node's inspect flag. Kibana's development mode will start three processes on ports `9229`, `9230`, and `9231`. Chrome's developer tools need to be configured to connect to all three connections. Add `localhost:` for each Kibana process in Chrome's developer tools connection tab. -### Instrumenting with Elastic APM +#### Instrumenting with Elastic APM Kibana ships with the [Elastic APM Node.js Agent](https://github.com/elastic/apm-agent-nodejs) built-in for debugging purposes. Its default configuration is meant to be used by core Kibana developers only, but it can easily be re-configured to your needs. @@ -501,13 +519,13 @@ ELASTIC_APM_ACTIVE=true yarn start Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics. The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana. -### Unit testing frameworks +#### Unit testing frameworks Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still exist in Mocha but all new unit tests should be written in Jest. Mocha tests are contained in `__tests__` directories. Whereas Jest tests are stored in the same directory as source code files with the `.test.js` suffix. -### Running specific Kibana tests +#### Running specific Kibana tests The following table outlines possible test file locations and how to invoke them: @@ -540,7 +558,7 @@ Test runner arguments: yarn test:ftr:runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets' ``` -### Debugging Unit Tests +#### Debugging Unit Tests The standard `yarn test` task runs several sub tasks and can take several minutes to complete, making debugging failures pretty painful. In order to ease the pain specialized tasks provide alternate methods for running the tests. @@ -567,7 +585,7 @@ In the screenshot below, you'll notice the URL is `localhost:9876/debug.html`. Y ![Browser test debugging](http://i.imgur.com/DwHxgfq.png) -### Unit Testing Plugins +#### Unit Testing Plugins This should work super if you're using the [Kibana plugin generator](https://github.com/elastic/kibana/tree/master/packages/kbn-plugin-generator). If you're not using the generator, well, you're on your own. We suggest you look at how the generator works. @@ -578,7 +596,7 @@ yarn test:mocha yarn test:karma:debug # remove the debug flag to run them once and close ``` -### Automated Accessibility Testing +#### Automated Accessibility Testing To run the tests locally: @@ -595,11 +613,11 @@ can be run locally using their browser plugins: - [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US) - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) -### Cross-browser Compatibility +#### Cross-browser Compatibility -#### Testing Compatibility Locally +##### Testing Compatibility Locally -##### Testing IE on OS X +###### Testing IE on OS X * [Download VMWare Fusion](http://www.vmware.com/products/fusion/fusion-evaluation.html). * [Download IE virtual machines](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/#downloads) for VMWare. @@ -610,7 +628,7 @@ can be run locally using their browser plugins: * Now you can run your VM, open the browser, and navigate to `http://computer.local:5601` to test Kibana. * Alternatively you can use browserstack -#### Running Browser Automation Tests +##### Running Browser Automation Tests [Read about the `FunctionalTestRunner`](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html) to learn more about how you can run and develop functional tests for Kibana core and plugins. From 761465bc77d66a9449be676873433bf148ccb493 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 29 May 2020 18:22:43 +0200 Subject: [PATCH 05/69] clean up kibana-app ownership (#67780) --- .github/CODEOWNERS | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 42bf7662ff2e1..c3da7c7f00e96 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,10 +6,7 @@ /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app -/src/legacy/server/url_shortening/ @elastic/kibana-app -/src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app From 1d5933b9a6de41a455f3ab949199f6bb7ae20b2e Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 29 May 2020 09:40:46 -0700 Subject: [PATCH 06/69] Changed AlertsClient to use ActionsClient instead of direct interaction with the `action` saved objects (#67562) --- .../actions/server/actions_client.mock.ts | 1 + .../actions/server/actions_client.test.ts | 68 +++++++++++++ .../plugins/actions/server/actions_client.ts | 40 +++++++- .../alerting/server/alerts_client.test.ts | 97 +++++++------------ .../plugins/alerting/server/alerts_client.ts | 86 ++++++---------- .../server/alerts_client_factory.test.ts | 15 ++- .../alerting/server/alerts_client_factory.ts | 14 +-- x-pack/plugins/alerting/server/plugin.ts | 2 +- 8 files changed, 192 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 64b43e1ab6bbc..a2b64e49f76e3 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -16,6 +16,7 @@ const createActionsClientMock = () => { delete: jest.fn(), update: jest.fn(), getAll: jest.fn(), + getBulk: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 0132cc8bdb01a..bf55a1c18d169 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -423,6 +423,74 @@ describe('getAll()', () => { }); }); +describe('getBulk()', () => { + test('calls getBulk savedObjectsClient with parameters', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + references: [], + }, + ], + }); + scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ + aggregations: { + '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, + }, + }); + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + const result = await actionsClient.getBulk(['1', 'testPreconfigured']); + expect(result).toEqual([ + { + actionTypeId: '.slack', + config: { + foo: 'bar', + }, + id: 'testPreconfigured', + isPreconfigured: true, + name: 'test', + secrets: {}, + }, + { + actionTypeId: 'test', + config: { + foo: 'bar', + }, + id: '1', + isPreconfigured: false, + name: 'test', + }, + ]); + }); +}); + describe('delete()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index c9052cf53d948..48703f01f5509 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import Boom from 'boom'; import { IScopedClusterClient, SavedObjectsClientContract, @@ -193,6 +193,44 @@ export class ActionsClient { ); } + /** + * Get bulk actions with preconfigured list + */ + public async getBulk(ids: string[]): Promise { + const actionResults = new Array(); + for (const actionId of ids) { + const action = this.preconfiguredActions.find( + (preconfiguredAction) => preconfiguredAction.id === actionId + ); + if (action !== undefined) { + actionResults.push(action); + } + } + + // Fetch action objects in bulk + // Excluding preconfigured actions to avoid an not found error, which is already added + const actionSavedObjectsIds = [ + ...new Set( + ids.filter( + (actionId) => !actionResults.find((actionResult) => actionResult.id === actionId) + ) + ), + ]; + + const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' })); + const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + + for (const action of bulkGetResult.saved_objects) { + if (action.error) { + throw Boom.badRequest( + `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` + ); + } + actionResults.push(actionFromSavedObject(action)); + } + return actionResults; + } + /** * Delete action */ diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index fa86c27651136..12106100602e7 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -13,6 +13,7 @@ import { TaskStatus } from '../../../plugins/task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { actionsClientMock } from '../../actions/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -30,7 +31,7 @@ const alertsClientParams = { invalidateAPIKey: jest.fn(), logger: loggingServiceMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, - preconfiguredActions: [], + getActionsClient: jest.fn(), }; beforeEach(() => { @@ -42,6 +43,34 @@ beforeEach(() => { }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + isPreconfigured: false, + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + { + id: '2', + isPreconfigured: false, + actionTypeId: 'test2', + name: 'test2', + config: { + foo: 'bar', + }, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); @@ -97,18 +126,6 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - ], - }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -297,26 +314,6 @@ describe('create()', () => { }, ], }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -435,16 +432,6 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ - { - id: '1', - type: 'action', - }, - { - id: '2', - type: 'action', - }, - ]); }); test('creates a disabled alert', async () => { @@ -549,7 +536,9 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockRejectedValueOnce(new Error('Test Error')); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); @@ -1903,26 +1892,6 @@ describe('update()', () => { }); test('updates given parameters', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); savedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index e43939e2f44c3..382e9d1a616ad 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -13,7 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; -import { PreConfiguredAction } from '../../actions/server'; +import { ActionsClient } from '../../actions/server'; import { Alert, PartialAlert, @@ -24,7 +24,6 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, - RawAlertAction, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -56,7 +55,7 @@ interface ConstructorOptions { getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; - preconfiguredActions: PreConfiguredAction[]; + getActionsClient: () => Promise; } export interface FindOptions { @@ -127,7 +126,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - private preconfiguredActions: PreConfiguredAction[]; + private readonly getActionsClient: () => Promise; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; constructor({ @@ -141,7 +140,7 @@ export class AlertsClient { createAPIKey, invalidateAPIKey, encryptedSavedObjectsClient, - preconfiguredActions, + getActionsClient, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -153,7 +152,7 @@ export class AlertsClient { this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; - this.preconfiguredActions = preconfiguredActions; + this.getActionsClient = getActionsClient; } public async create({ data, options }: CreateOptions): Promise { @@ -600,7 +599,7 @@ export class AlertsClient { actions: RawAlert['actions'], references: SavedObjectReference[] ) { - return actions.map((action, i) => { + return actions.map((action) => { const reference = references.find((ref) => ref.name === action.actionRef); if (!reference) { throw new Error(`Reference ${action.actionRef} not found`); @@ -666,58 +665,31 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionMap = new Map(); - // map preconfigured actions - for (const alertAction of alertActions) { - const action = this.preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === alertAction.id - ); - if (action !== undefined) { - actionMap.set(action.id, action); - } - } - // Fetch action objects in bulk - // Excluding preconfigured actions to avoid an not found error, which is already mapped - const actionIds = [ - ...new Set( - alertActions - .filter((alertAction) => !actionMap.has(alertAction.id)) - .map((alertAction) => alertAction.id) - ), - ]; - if (actionIds.length > 0) { - const bulkGetOpts = actionIds.map((id) => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); - - for (const action of bulkGetResult.saved_objects) { - if (action.error) { - throw Boom.badRequest( - `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` - ); - } - actionMap.set(action.id, action); - } - } - // Extract references and set actionTypeId + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionResults = await actionsClient.getBulk(actionIds); const references: SavedObjectReference[] = []; const actions = alertActions.map(({ id, ...alertAction }, i) => { - const actionRef = `action_${i}`; - references.push({ - id, - name: actionRef, - type: 'action', - }); - const actionMapValue = actionMap.get(id); - // if action is a save object, than actionTypeId should be under attributes property - // if action is a preconfigured, than actionTypeId is the action property - const actionTypeId = actionIds.find((actionId) => actionId === id) - ? (actionMapValue as SavedObject>).attributes.actionTypeId - : (actionMapValue as RawAlertAction).actionTypeId; - return { - ...alertAction, - actionRef, - actionTypeId, - }; + const actionResultValue = actionResults.find((action) => action.id === id); + if (actionResultValue) { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + return { + ...alertAction, + actionRef, + actionTypeId: actionResultValue.actionTypeId, + }; + } else { + return { + ...alertAction, + actionRef: '', + actionTypeId: '', + }; + } }); return { actions, diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index cc792d11c890d..d1a7c60bb9a68 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -13,6 +13,7 @@ import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { AuthenticatedUser } from '../../../plugins/security/public'; import { securityMock } from '../../../plugins/security/server/mocks'; +import { actionsMock } from '../../actions/server/mocks'; jest.mock('./alerts_client'); @@ -25,7 +26,7 @@ const alertsClientFactoryParams: jest.Mocked = { getSpaceId: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), - preconfiguredActions: [], + actions: actionsMock.createStart(), }; const fakeRequest = ({ headers: {}, @@ -65,7 +66,7 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, - preconfiguredActions: [], + getActionsClient: expect.any(Function), }); }); @@ -95,6 +96,16 @@ test('getUserName() returns a name when security is enabled', async () => { expect(userNameResult).toEqual('bob'); }); +test('getActionsClient() returns ActionsClient', async () => { + const factory = new AlertsClientFactory(); + factory.initialize(alertsClientFactoryParams); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; + + const actionsClient = await constructorCall.getActionsClient(); + expect(actionsClient).not.toBe(null); +}); + test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 913b4e2e81fe1..2924736330abd 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PreConfiguredAction } from '../../actions/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; @@ -20,7 +20,7 @@ export interface AlertsClientFactoryOpts { getSpaceId: (request: KibanaRequest) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - preconfiguredActions: PreConfiguredAction[]; + actions: ActionsPluginStartContract; } export class AlertsClientFactory { @@ -32,7 +32,7 @@ export class AlertsClientFactory { private getSpaceId!: (request: KibanaRequest) => string | undefined; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; - private preconfiguredActions!: PreConfiguredAction[]; + private actions!: ActionsPluginStartContract; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -46,14 +46,14 @@ export class AlertsClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; - this.preconfiguredActions = options.preconfiguredActions; + this.actions = options.actions; } public create( request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract ): AlertsClient { - const { securityPluginSetup } = this; + const { securityPluginSetup, actions } = this; const spaceId = this.getSpaceId(request); return new AlertsClient({ spaceId, @@ -104,7 +104,9 @@ export class AlertsClientFactory { result: invalidateAPIKeyResult, }; }, - preconfiguredActions: this.preconfiguredActions, + async getActionsClient() { + return actions.getActionsClientWithRequest(request); + }, }); } } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 33fb8d9e0d212..e789e655774a0 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -216,7 +216,7 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return spaces?.getSpaceId(request); }, - preconfiguredActions: plugins.actions.preconfiguredActions, + actions: plugins.actions, }); taskRunnerFactory.initialize({ From 6b7b0cbc44769046a969e7789203f8f148747e33 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Fri, 29 May 2020 13:33:46 -0400 Subject: [PATCH 07/69] [Endpoint]EMT: temporarily skip test till package update. (#67778) [Endpoint]EMT: temporarily skip test till package update. --- x-pack/test/api_integration/apis/endpoint/alerts/index.ts | 2 +- .../test/api_integration/apis/endpoint/alerts/index_pattern.ts | 2 +- x-pack/test/api_integration/apis/endpoint/metadata.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts index 155513aefc609..ecdee09ce7edf 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts @@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) { let nullableEventId = ''; - describe('Endpoint alert API', () => { + describe.skip('Endpoint alert API', () => { describe('when data is in elasticsearch', () => { before(async () => { await esArchiver.load('endpoint/alerts/api_feature'); diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts index e87b063453054..df1cbcfe28e7b 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Endpoint index pattern API', () => { + describe.skip('Endpoint index pattern API', () => { it('should retrieve the index pattern for events', async () => { const { body } = await supertest.get('/api/endpoint/index_pattern/events').expect(200); expect(body.indexPattern).to.eql('events-endpoint-*'); diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 5c4bb52b8d9e2..c01919f60a922 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -14,7 +14,7 @@ const numberOfHostsInFixture = 3; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + describe.skip('test metadata api', () => { describe('POST /api/endpoint/metadata when index is empty', () => { it('metadata api should return empty result when index is empty', async () => { await esArchiver.unload('endpoint/metadata/api_feature'); From 6288096f622489877d20ed3de381e8df37a574f0 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 29 May 2020 10:34:58 -0700 Subject: [PATCH 08/69] [kbn/optimizer] use execa to fork workers (#67730) Co-authored-by: spalger --- packages/kbn-optimizer/package.json | 1 + .../src/optimizer/observe_worker.ts | 14 ++++---- packages/kbn-pm/dist/index.js | 34 +++++++++---------- yarn.lock | 8 ++--- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b7c9a63897bf9..7bd7a236a43aa 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -28,6 +28,7 @@ "cpy": "^8.0.0", "css-loader": "^3.4.2", "del": "^5.1.0", + "execa": "^4.0.2", "file-loader": "^4.2.0", "istanbul-instrumenter-loader": "^3.0.1", "jest-diff": "^25.1.0", diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index f5c944cefb76f..c929cf62d1bb0 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -17,10 +17,10 @@ * under the License. */ -import { fork, ChildProcess } from 'child_process'; import { Readable } from 'stream'; import { inspect } from 'util'; +import execa from 'execa'; import * as Rx from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; @@ -42,7 +42,7 @@ export interface WorkerStarted { export type WorkerStatus = WorkerStdio | WorkerStarted; interface ProcResource extends Rx.Unsubscribable { - proc: ChildProcess; + proc: execa.ExecaChildProcess; } const isNumeric = (input: any) => String(input).match(/^[0-9]+$/); @@ -70,20 +70,22 @@ function usingWorkerProc( config: OptimizerConfig, workerConfig: WorkerConfig, bundles: Bundle[], - fn: (proc: ChildProcess) => Rx.Observable + fn: (proc: execa.ExecaChildProcess) => Rx.Observable ) { return Rx.using( (): ProcResource => { const args = [JSON.stringify(workerConfig), JSON.stringify(bundles.map((b) => b.toSpec()))]; - const proc = fork(require.resolve('../worker/run_worker'), args, { - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - execArgv: [ + const proc = execa.node(require.resolve('../worker/run_worker'), args, { + nodeOptions: [ ...(inspectFlag && config.inspectWorkers ? [`${inspectFlag}=${inspectPortCounter++}`] : []), ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), ], + buffer: false, + stderr: 'pipe', + stdout: 'pipe', }); return { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index baaac3d8d4a86..21fff4d85ece6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -34812,10 +34812,11 @@ const makeError = ({ const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); const execaMessage = `Command ${prefix}: ${command}`; - const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage; + const isError = Object.prototype.toString.call(error) === '[object Error]'; + const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); - if (error instanceof Error) { + if (isError) { error.originalMessage = error.message; error.message = message; } else { @@ -36263,25 +36264,24 @@ module.exports = function (/*streams...*/) { "use strict"; -const mergePromiseProperty = (spawned, promise, property) => { - // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' ? - (...args) => promise()[property](...args) : - promise[property].bind(promise); - Object.defineProperty(spawned, property, { - value, - writable: true, - enumerable: false, - configurable: true - }); -}; +const nativePromisePrototype = (async () => {})().constructor.prototype; +const descriptors = ['then', 'catch', 'finally'].map(property => [ + property, + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property) +]); // The return value is a mixin of `childProcess` and `Promise` const mergePromise = (spawned, promise) => { - mergePromiseProperty(spawned, promise, 'then'); - mergePromiseProperty(spawned, promise, 'catch'); - mergePromiseProperty(spawned, promise, 'finally'); + for (const [property, descriptor] of descriptors) { + // Starting the main `promise` is deferred to avoid consuming streams + const value = typeof promise === 'function' ? + (...args) => Reflect.apply(descriptor.value, promise(), args) : + descriptor.value.bind(promise); + + Reflect.defineProperty(spawned, property, {...descriptor, value}); + } + return spawned; }; diff --git a/yarn.lock b/yarn.lock index e5463de13ea00..10f338b414456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13092,10 +13092,10 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" - integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA== +execa@^4.0.0, execa@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" + integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" From fbb5f3169892803f42ace5fa0ef02c9362126c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 29 May 2020 20:00:33 +0200 Subject: [PATCH 09/69] =?UTF-8?q?[APM]=20Don=E2=80=99t=20run=20eslint=20on?= =?UTF-8?q?=20cypress=20snapshots=20(#67451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Don’t run eslint on cypress snapshots * ignore cypress videos * Fix interactive command * Fix gitignore * Use echo everywhere Co-authored-by: Elastic Machine --- .eslintignore | 3 +- .gitignore | 8 ++-- x-pack/plugins/apm/e2e/.gitignore | 3 +- x-pack/plugins/apm/e2e/run-e2e.sh | 72 +++++++++++++++++++++---------- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/.eslintignore b/.eslintignore index c3d7930732fa2..fbdd70703f3c4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,7 +26,8 @@ target /src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** -/x-pack/plugins/apm/e2e/cypress/**/snapshots.js +/x-pack/plugins/apm/e2e/**/snapshots.js +/x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/canvas/shareable_runtime/build diff --git a/.gitignore b/.gitignore index f843609d32f7e..b3911d0f8d0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,8 @@ package-lock.json *.sublime-* npm-debug.log* .tern-project -x-pack/plugins/apm/tsconfig.json -apm.tsconfig.json -/x-pack/legacy/plugins/apm/e2e/snapshots.js -/x-pack/plugins/apm/e2e/snapshots.js .nyc_output + +# apm plugin +/x-pack/plugins/apm/tsconfig.json +apm.tsconfig.json diff --git a/x-pack/plugins/apm/e2e/.gitignore b/x-pack/plugins/apm/e2e/.gitignore index 9eb738ede51e3..5042f0bca0300 100644 --- a/x-pack/plugins/apm/e2e/.gitignore +++ b/x-pack/plugins/apm/e2e/.gitignore @@ -1,4 +1,5 @@ cypress/screenshots/* -cypress/videos/* cypress/test-results +cypress/videos/* +/snapshots.js tmp diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 157c42cc7e4ee..ae764d171c45c 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -26,20 +26,23 @@ cd ${E2E_DIR} # # Ask user to start Kibana ################################################## -echo "\n${bold}To start Kibana please run the following command:${normal} +echo "" # newline +echo "${bold}To start Kibana please run the following command:${normal} node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/plugins/apm/e2e/ci/kibana.e2e.yml" # # Create tmp folder ################################################## -echo "\n${bold}Temporary folder${normal}" -echo "Temporary files will be stored in: ${TMP_DIR}" +echo "" # newline +echo "${bold}Temporary folder${normal}" +echo "Temporary files will be stored in: ${E2E_DIR}${TMP_DIR}" mkdir -p ${TMP_DIR} # # apm-integration-testing ################################################## -printf "\n${bold}apm-integration-testing (logs: ${TMP_DIR}/apm-it.log)\n${normal}" +echo "" # newline +echo "${bold}apm-integration-testing (logs: ${E2E_DIR}${TMP_DIR}/apm-it.log)${normal}" # pull if folder already exists if [ -d ${APM_IT_DIR} ]; then @@ -54,7 +57,7 @@ fi # Stop if clone/pull failed if [ $? -ne 0 ]; then - printf "\n⚠️ Initializing apm-integration-testing failed. \n" + echo "⚠️ Initializing apm-integration-testing failed." exit 1 fi @@ -71,23 +74,34 @@ ${APM_IT_DIR}/scripts/compose.py start master \ # Stop if apm-integration-testing failed to start correctly if [ $? -ne 0 ]; then - printf "⚠️ apm-integration-testing could not be started.\n" - printf "Please see the logs in ${TMP_DIR}/apm-it.log\n\n" - printf "As a last resort, reset docker with:\n\n cd ${APM_IT_DIR} && scripts/compose.py stop && docker system prune --all --force --volumes\n" + echo "⚠️ apm-integration-testing could not be started" + echo "" # newline + echo "As a last resort, reset docker with:" + echo "" # newline + echo "cd ${E2E_DIR}${APM_IT_DIR} && scripts/compose.py stop && docker system prune --all --force --volumes" + echo "" # newline + + # output logs for excited docker containers + cd ${APM_IT_DIR} && docker-compose ps --filter "status=exited" -q | xargs -L1 docker logs --tail=10 && cd - + + echo "" # newline + echo "Find the full logs in ${E2E_DIR}${TMP_DIR}/apm-it.log" exit 1 fi # # Cypress ################################################## -echo "\n${bold}Cypress (logs: ${TMP_DIR}/e2e-yarn.log)${normal}" +echo "" # newline +echo "${bold}Cypress (logs: ${E2E_DIR}${TMP_DIR}/e2e-yarn.log)${normal}" echo "Installing cypress dependencies " yarn &> ${TMP_DIR}/e2e-yarn.log # # Static mock data ################################################## -printf "\n${bold}Static mock data (logs: ${TMP_DIR}/ingest-data.log)\n${normal}" +echo "" # newline +echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${normal}" # Download static data if not already done if [ ! -e "${TMP_DIR}/events.json" ]; then @@ -102,16 +116,32 @@ curl --silent --user admin:changeme -XDELETE "localhost:${ELASTICSEARCH_PORT}/ap # Ingest data into APM Server node ingest-data/replay.js --server-url http://localhost:$APM_SERVER_PORT --events ${TMP_DIR}/events.json 2>> ${TMP_DIR}/ingest-data.log -# Stop if not all events were ingested correctly +# Abort if not all events were ingested correctly if [ $? -ne 0 ]; then - printf "\n⚠️ Not all events were ingested correctly. This might affect test tests. \n" + echo "⚠️ Not all events were ingested correctly. This might affect test tests." + echo "Aborting. Please try again." + echo "" # newline + echo "Full logs in ${E2E_DIR}${TMP_DIR}/ingest-data.log:" + + # output logs for excited docker containers + cd ${APM_IT_DIR} && docker-compose ps --filter "status=exited" -q | xargs -L1 docker logs --tail=3 && cd - + + # stop docker containers + cd ${APM_IT_DIR} && ./scripts/compose.py stop > /dev/null && cd - exit 1 fi +# create empty snapshot file if it doesn't exist +SNAPSHOTS_FILE=cypress/integration/snapshots.js +if [ ! -f ${SNAPSHOTS_FILE} ]; then + echo "{}" > ${SNAPSHOTS_FILE} +fi + # # Wait for Kibana to start ################################################## -echo "\n${bold}Waiting for Kibana to start...${normal}" +echo "" # newline +echo "${bold}Waiting for Kibana to start...${normal}" echo "Note: you need to start Kibana manually. Find the instructions at the top." yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/status > /dev/null @@ -119,12 +149,13 @@ yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/ ## See: https://github.com/elastic/kibana/issues/66326 if [ -e kibana.log ] ; then grep -m 1 "http server running" <(tail -f -n +1 kibana.log) - echo "\n✅ Kibana server running...\n" + echo "✅ Kibana server running..." grep -m 1 "bundles compiled successfully" <(tail -f -n +1 kibana.log) - echo "\n✅ Kibana bundles have been compiled...\n" + echo "✅ Kibana bundles have been compiled..." fi -echo "\n✅ Setup completed successfully. Running tests...\n" + +echo "✅ Setup completed successfully. Running tests..." # # run cypress tests @@ -134,9 +165,6 @@ yarn cypress run --config pageLoadTimeout=100000,watchForFileChanges=true # # Run interactively ################################################## -echo " - -${bold}If you want to run the test interactively, run:${normal} - -yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true -" +echo "${bold}If you want to run the test interactively, run:${normal}" +echo "" # newline +echo "cd ${E2E_DIR} && yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true" From 81d55f8822a29b6c1310bdb4f52a66f1fdcbb4ba Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 29 May 2020 14:58:40 -0400 Subject: [PATCH 10/69] [CI] Bump chromedriver and use DETECT_CHROMEDRIVER_VERSION (#67642) --- package.json | 2 +- src/dev/ci_setup/setup_env.sh | 10 +++++++++- yarn.lock | 8 ++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2dca52121d056..cc1f7eb6c1dd3 100644 --- a/package.json +++ b/package.json @@ -408,7 +408,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^81.0.0", + "chromedriver": "^83.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index d9d0528748dc0..343ff47199375 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -128,9 +128,17 @@ export GECKODRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfuncti export CHROMEDRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" - export CHECKS_REPORTER_ACTIVE=false +# This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed +if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then + echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" + export DETECT_CHROMEDRIVER_VERSION=true + export CHROMEDRIVER_FORCE_DOWNLOAD=true +else + echo "Chrome not detected, installing default chromedriver binary for the package version" +fi + ### only run on pr jobs for elastic/kibana, checks-reporter doesn't work for other repos if [[ "$ghprbPullId" && "$ghprbGhRepository" == 'elastic/kibana' ]] ; then export CHECKS_REPORTER_ACTIVE=true diff --git a/yarn.lock b/yarn.lock index 10f338b414456..5d47056857bbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8945,10 +8945,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^81.0.0: - version "81.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-81.0.0.tgz#690ba333aedf2b4c4933b6590c3242d3e5f28f3c" - integrity sha512-BA++IQ7O1FzHmNpzMlOfLiSBvPZ946uuhtJjZHEIr/Gb+Ha9jiuGbHiT45l6O3XGbQ8BAwvbmdisjl4rTxro4A== +chromedriver@^83.0.0: + version "83.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-83.0.0.tgz#75d7d838e58014658c3990089464166fef951926" + integrity sha512-AePp9ykma+z4aKPRqlbzvVlc22VsQ6+rgF+0aL3B5onHOncK18dWSkLrSSJMczP/mXILN9ohGsvpuTwoRSj6OQ== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" From a63adabd3816e8a10cf35cf94c576a2f709bb30a Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 29 May 2020 12:15:05 -0700 Subject: [PATCH 11/69] skip flaky suite (#66976) --- x-pack/test/accessibility/apps/home.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 1f05ff676e3a0..fe698acec322a 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -12,7 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const globalNav = getService('globalNav'); - describe('Kibana Home', () => { + // FLAKY: https://github.com/elastic/kibana/issues/66976 + describe.skip('Kibana Home', () => { before(async () => { await PageObjects.common.navigateToApp('home'); }); From 87c34cf10f9edd7c446312190fb5e3051962103a Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 29 May 2020 13:50:33 -0700 Subject: [PATCH 12/69] [DOCS] Identifies cloud settings for ML (#67573) --- docs/settings/ml-settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 24e38e73bca9b..1753e1fdecb95 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -13,7 +13,7 @@ enabled by default. [cols="2*<"] |=== -| `xpack.ml.enabled` +| `xpack.ml.enabled` {ess-icon} | Set to `true` (default) to enable {kib} {ml-features}. + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} From 402018856eea66a88785256d1a603a011795613a Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 29 May 2020 14:21:14 -0700 Subject: [PATCH 13/69] [kbn/optimizer] update public path before imports (#67561) Co-authored-by: spalger --- .../mock_repo/plugins/bar/public/index.scss | 3 +++ .../mock_repo/plugins/bar/public/index.ts | 1 + .../basic_optimization.test.ts.snap | 6 ++--- .../basic_optimization.test.ts | 19 ++++++++++----- .../src/worker/webpack.config.ts | 12 +++++++--- packages/kbn-ui-shared-deps/package.json | 2 ++ .../kbn-ui-shared-deps/public_path_loader.js | 10 +++++++- .../public_path_module_creator.js | 24 +++++++++++++++++++ 8 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss create mode 100644 packages/kbn-ui-shared-deps/public_path_module_creator.js diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss new file mode 100644 index 0000000000000..563d20e99ce82 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss @@ -0,0 +1,3 @@ +body { + color: green; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 817c4796562e8..7ddd10f4a388f 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -18,6 +18,7 @@ */ import './legacy/styles.scss'; +import './index.scss'; import { fooLibFn } from '../../foo/public/index'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 8bf5eb72523ff..2814ab32017d2 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -55,8 +55,8 @@ OptimizerConfig { } `; -exports[`prepares assets for distribution: 1 async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],[,function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}]]);"`; +exports[`prepares assets for distribution: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i value.split(REPO_ROOT).join('').replace(/\\/g, '/'), + test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT), +}); const log = new ToolingLog({ level: 'error', @@ -129,13 +132,14 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const foo = config.bundles.find((b) => b.id === 'foo')!; expect(foo).toBeTruthy(); foo.cache.refresh(); - expect(foo.cache.getModuleCount()).toBe(4); + expect(foo.cache.getModuleCount()).toBe(5); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -143,14 +147,15 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(bar).toBeTruthy(); bar.cache.refresh(); expect(bar.cache.getModuleCount()).toBe( - // code + styles + style/css-loader runtimes - 15 + // code + styles + style/css-loader runtimes + public path updater + 21 ); expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, @@ -159,6 +164,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); }); @@ -207,7 +213,7 @@ it('prepares assets for distribution', async () => { expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( 'plugins/foo/target/public/1.plugin.js', - '1 async bundle' + 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); }); @@ -217,6 +223,7 @@ it('prepares assets for distribution', async () => { */ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabel: string) => { const raw = Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, filePath), 'utf8'); + expect(raw).toMatchSnapshot(snapshotLabel); // Verify the brotli variant matches diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 0c9a5b0a75687..763f1d515804f 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -17,6 +17,7 @@ * under the License. */ +import Fs from 'fs'; import Path from 'path'; import normalizePath from 'normalize-path'; @@ -86,12 +87,17 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { } export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { + const extensions = ['.js', '.ts', '.tsx', '.json']; + const entryExtension = extensions.find((ext) => + Fs.existsSync(Path.resolve(bundle.contextDir, bundle.entry) + ext) + ); + const commonConfig: webpack.Configuration = { node: { fs: 'empty' }, context: bundle.contextDir, cache: true, entry: { - [bundle.id]: bundle.entry, + [bundle.id]: `${bundle.entry}${entryExtension}`, }, devtool: worker.dist ? false : '#cheap-source-map', @@ -144,7 +150,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { rules: [ { - include: Path.join(bundle.contextDir, bundle.entry), + include: [`${Path.resolve(bundle.contextDir, bundle.entry)}${entryExtension}`], loader: UiSharedDeps.publicPathLoader, options: { key: bundle.id, @@ -292,7 +298,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { }, resolve: { - extensions: ['.js', '.ts', '.tsx', '.json'], + extensions, mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index d34fe3624ab26..4e6bec92a65e4 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -35,6 +35,8 @@ "devDependencies": { "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "loader-utils": "^1.2.3", + "val-loader": "^1.1.1", "css-loader": "^3.4.2", "del": "^5.1.0", "webpack": "^4.41.5" diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js index fceebd6d6b3a1..cdebdcb4f0422 100644 --- a/packages/kbn-ui-shared-deps/public_path_loader.js +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -17,7 +17,15 @@ * under the License. */ +const Qs = require('querystring'); +const { stringifyRequest } = require('loader-utils'); + +const VAL_LOADER = require.resolve('val-loader'); +const MODULE_CREATOR = require.resolve('./public_path_module_creator'); + module.exports = function (source) { const options = this.query; - return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; + const valOpts = Qs.stringify({ key: options.key }); + const req = `${VAL_LOADER}?${valOpts}!${MODULE_CREATOR}`; + return `import ${stringifyRequest(this, req)};${source}`; }; diff --git a/packages/kbn-ui-shared-deps/public_path_module_creator.js b/packages/kbn-ui-shared-deps/public_path_module_creator.js new file mode 100644 index 0000000000000..1cb9989432178 --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_module_creator.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = function ({ key }) { + return { + code: `__webpack_public_path__ = window.__kbnPublicPath__['${key}']`, + }; +}; From 3c40b97794640afda4120c2f91c62ac660cf41aa Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 29 May 2020 15:24:04 -0700 Subject: [PATCH 14/69] [DOCS] Link machine learning settings to advanced settings (#67572) --- docs/settings/ml-settings.asciidoc | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 1753e1fdecb95..83443636fa633 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -6,7 +6,7 @@ ++++ You do not need to configure any settings to use {kib} {ml-features}. They are -enabled by default. +enabled by default. [[general-ml-settings-kb]] ==== General {ml} settings @@ -23,13 +23,7 @@ enabled by default. |=== -[[data-visualizer-settings]] -==== {data-viz} settings +[[advanced-ml-settings-kb]] +==== Advanced {ml} settings -[cols="2*<"] -|=== -| `xpack.ml.file_data_visualizer.max_file_size` - | Sets the file size limit when importing data in the {data-viz}. The default - value is `100MB`. The highest supported value for this setting is `1GB`. - -|=== +Refer to <>. \ No newline at end of file From 39902870c8b6756e4f897633defbb30fcb6fd382 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 29 May 2020 15:31:17 -0700 Subject: [PATCH 15/69] [Reporting]: Move router + license checks to new platform (#66331) * WIP: Move routes to new API, license and other checks inbound * Move license checks over to np licensing observable * Fix license checks + remove older modules * Fixing check_license tests, move to TS/Jest * Fix licensing setup for mocks * Move job.test.ts over to np * WIP: move user checks to higher-order func * Move more handler logic over to Response factory vs Boom * Major refactor to consolidate types, remove facades, and udpate helpers * Fix validation for dates in immediate exports * Linter fix on check license test * Fix job generation tests * Move deps => setupDeps * fix api test * fix jobs test * authorized_user_pre_routing and tests * Fixing duplicate identifiers * Fix licensing implementation changes * WIP: Moving license over to async/observables * Fix disabled-security case * finish auth_user_pre_routing cleanup - no more license check * WIP: Fixing final api tests * Trying to get schema differences in alignment * Reverting back to previous generation handler * Fix final API tests * Final API test fixes, few more hardening tests and better error messages * Simplify lower-level module implementation (core only interface) + test updates * Push some core logic into plugin * Move some core logic up to plugin * Marking private setupDeps + downstream fixes * revert logger as a param Co-authored-by: Timothy Sullivan --- .../plugins/reporting/common/constants.ts | 1 + .../export_types/csv/server/create_job.ts | 19 +- .../server/create_job/create_job.ts | 18 +- .../server/execute_job.ts | 22 +- .../server/lib/generate_csv.ts | 14 +- .../server/lib/generate_csv_search.ts | 34 +- .../server/lib/get_filters.ts | 2 +- .../server/lib/get_job_params_from_request.ts | 12 +- .../png/server/create_job/index.ts | 21 +- .../printable_pdf/server/create_job/index.ts | 19 +- .../legacy/plugins/reporting/server/core.ts | 86 ++- .../legacy/plugins/reporting/server/legacy.ts | 3 +- .../server/lib/__tests__/check_license.js | 147 ----- .../server/lib/check_license.test.ts | 192 ++++++ .../reporting/server/lib/check_license.ts | 56 +- .../reporting/server/lib/enqueue_job.ts | 24 +- .../plugins/reporting/server/lib/get_user.ts | 13 +- .../plugins/reporting/server/lib/index.ts | 2 +- .../reporting/server/lib/jobs_query.ts | 31 +- .../legacy/plugins/reporting/server/plugin.ts | 33 +- .../server/routes/generate_from_jobparams.ts | 125 ++-- .../routes/generate_from_savedobject.ts | 81 +-- .../generate_from_savedobject_immediate.ts | 114 ++-- .../server/routes/generation.test.ts | 274 +++++---- .../reporting/server/routes/generation.ts | 84 +-- .../plugins/reporting/server/routes/index.ts | 14 +- .../reporting/server/routes/jobs.test.ts | 564 +++++++++--------- .../plugins/reporting/server/routes/jobs.ts | 330 +++++----- .../lib/authorized_user_pre_routing.test.js | 175 ------ .../lib/authorized_user_pre_routing.test.ts | 134 +++++ .../routes/lib/authorized_user_pre_routing.ts | 74 ++- .../server/routes/lib/get_document_payload.ts | 14 +- .../server/routes/lib/job_response_handler.ts | 77 ++- .../routes/lib/make_request_facade.test.ts | 62 -- .../server/routes/lib/make_request_facade.ts | 32 - .../lib/reporting_feature_pre_routing.ts | 36 -- .../routes/lib/route_config_factories.ts | 130 ---- .../reporting/server/routes/types.d.ts | 13 +- .../legacy/plugins/reporting/server/types.ts | 21 +- .../create_mock_reportingplugin.ts | 20 +- .../test_helpers/create_mock_server.ts | 31 +- x-pack/plugins/reporting/kibana.json | 3 +- .../reporting/csv_job_params.ts | 2 +- 43 files changed, 1492 insertions(+), 1667 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js create mode 100644 x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js create mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index f30a7cc87f318..48483c79d1af2 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -27,6 +27,7 @@ export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/pdf', CONTENT_TYPE_CSV, 'image/png', + 'text/plain', ]; // See: diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts index 8320cd05aa2e7..c76b4afe727da 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ReportingCore } from '../../../server'; import { cryptoFactory } from '../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../server/types'; import { JobParamsDiscoverCsv } from '../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); + const setupDeps = reporting.getPluginSetupDeps(); return async function createJob( jobParams: JobParamsDiscoverCsv, - headers: ConditionalHeaders['headers'], - request: RequestFacade + context: RequestHandlerContext, + request: KibanaRequest ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(request.headers); - const savedObjectsClient = request.getSavedObjectsClient(); + const savedObjectsClient = context.core.savedObjects.client; const indexPatternSavedObject = await savedObjectsClient.get( 'index-pattern', jobParams.indexPatternId! @@ -36,7 +33,7 @@ export const createJobFactory: CreateJobFactory = ( jobParams: JobParamsType, - headers: Record, - req: RequestFacade + headers: KibanaRequest['headers'], + context: RequestHandlerContext, + req: KibanaRequest ) => Promise<{ type: string | null; title: string; @@ -46,21 +48,21 @@ export const createJobFactory: CreateJobFactory { const { savedObjectType, savedObjectId } = jobParams; const serializedEncryptedHeaders = await crypto.encrypt(headers); - const client = req.getSavedObjectsClient(); const { panel, title, visType }: VisData = await Promise.resolve() - .then(() => client.get(savedObjectType, savedObjectId)) + .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) .then(async (savedObject: SavedObject) => { const { attributes, references } = savedObject; const { kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, } = attributes as SavedSearchObjectAttributesJSON; - const { timerange } = req.payload as { timerange: TimeRangeParams }; + const { timerange } = req.body as { timerange: TimeRangeParams }; if (!kibanaSavedObjectMetaJSON) { throw new Error('Could not parse saved object data!'); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 5761a98ed160c..4ef7b8514b363 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,15 +5,11 @@ */ import { i18n } from '@kbn/i18n'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { ReportingCore } from '../../../server'; import { cryptoFactory, LevelLogger } from '../../../server/lib'; -import { - ExecuteJobFactory, - JobDocOutput, - JobDocPayload, - RequestFacade, -} from '../../../server/types'; +import { ExecuteJobFactory, JobDocOutput, JobDocPayload } from '../../../server/types'; import { CsvResultFromSearch } from '../../csv/types'; import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; import { createGenerateCsv } from './lib'; @@ -25,7 +21,8 @@ import { createGenerateCsv } from './lib'; export type ImmediateExecuteFn = ( jobId: null, job: JobDocPayload, - request: RequestFacade + context: RequestHandlerContext, + req: KibanaRequest ) => Promise; export const executeJobFactory: ExecuteJobFactory { // There will not be a jobID for "immediate" generation. // jobID is only for "queued" jobs @@ -58,10 +56,11 @@ export const executeJobFactory: ExecuteJobFactory; @@ -103,6 +102,7 @@ export const executeJobFactory: ExecuteJobFactory { }; export async function generateCsvSearch( - req: RequestFacade, reporting: ReportingCore, - logger: LevelLogger, + context: RequestHandlerContext, + req: KibanaRequest, searchPanel: SearchPanel, - jobParams: JobParamsDiscoverCsv + jobParams: JobParamsDiscoverCsv, + logger: LevelLogger ): Promise { - const savedObjectsClient = await reporting.getSavedObjectsClient( - KibanaRequest.from(req.getRawRequest()) - ); + const savedObjectsClient = context.core.savedObjects.client; const { indexPatternSavedObjectId, timerange } = searchPanel; - const savedSearchObjectAttr = searchPanel.attributes as SavedSearchObjectAttributes; + const savedSearchObjectAttr = searchPanel.attributes; const { indexPatternSavedObject } = await getDataSource( savedObjectsClient, indexPatternSavedObjectId @@ -153,9 +149,7 @@ export async function generateCsvSearch( const config = reporting.getConfig(); const elasticsearch = await reporting.getElasticsearchService(); - const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( - KibanaRequest.from(req.getRawRequest()) - ); + const { callAsCurrentUser } = elasticsearch.dataClient.asScoped(req); const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts index 071427f4dab64..4695bbd922581 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts @@ -22,7 +22,7 @@ export function getFilters( let timezone: string | null; if (indexPatternTimeField) { - if (!timerange) { + if (!timerange || !timerange.min || !timerange.max) { throw badRequest( `Time range params are required for index pattern [${indexPatternId}], using time field [${indexPatternTimeField}]` ); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts index 57d74ee0e1383..5aed02c10b961 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestFacade } from '../../../../server/types'; +import { KibanaRequest } from 'src/core/server'; import { JobParamsPanelCsv, JobParamsPostPayloadPanelCsv } from '../../types'; export function getJobParamsFromRequest( - request: RequestFacade, + request: KibanaRequest, opts: { isImmediate: boolean } ): JobParamsPanelCsv { - const { savedObjectType, savedObjectId } = request.params; - const { timerange, state } = request.payload as JobParamsPostPayloadPanelCsv; + const { savedObjectType, savedObjectId } = request.params as { + savedObjectType: string; + savedObjectId: string; + }; + const { timerange, state } = request.body as JobParamsPostPayloadPanelCsv; + const post = timerange || state ? { timerange, state } : undefined; return { diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts index b19513de29eee..ab492c21256eb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts @@ -5,28 +5,23 @@ */ import { validateUrls } from '../../../../common/validate_urls'; -import { ReportingCore } from '../../../../server'; import { cryptoFactory } from '../../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../server/types'; import { JobParamsPNG } from '../../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJob( - { objectType, title, relativeUrl, browserTimezone, layout }: JobParamsPNG, - headers: ConditionalHeaders['headers'], - request: RequestFacade + { objectType, title, relativeUrl, browserTimezone, layout }, + context, + req ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); validateUrls([relativeUrl]); @@ -37,7 +32,7 @@ export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJobFn( { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, - headers: ConditionalHeaders['headers'], - request: RequestFacade + context, + req ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); validateUrls(relativeUrls); return { - basePath: request.getBasePath(), + basePath: setupDeps.basePath(req), browserTimezone, forceNow: new Date().toISOString(), headers: serializedEncryptedHeaders, diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/legacy/plugins/reporting/server/core.ts index 8fb948a253c16..b89ef9e06b961 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/legacy/plugins/reporting/server/core.ts @@ -5,33 +5,35 @@ */ import * as Rx from 'rxjs'; -import { first, mapTo } from 'rxjs/operators'; +import { first, mapTo, map } from 'rxjs/operators'; import { ElasticsearchServiceSetup, KibanaRequest, - SavedObjectsClient, SavedObjectsServiceStart, UiSettingsServiceStart, + IRouter, + SavedObjectsClientContract, + BasePath, } from 'src/core/server'; -import { ReportingPluginSpecOptions } from '../'; -// @ts-ignore no module definition -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; -import { PLUGIN_ID } from '../common/constants'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; import { screenshotsObservableFactory } from '../export_types/common/lib/screenshots'; -import { ServerFacade } from '../server/types'; +import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; -import { checkLicenseFactory, getExportTypesRegistry, LevelLogger } from './lib'; +import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { EnqueueJobFn } from './lib/enqueue_job'; -import { registerRoutes } from './routes'; -import { ReportingSetupDeps } from './types'; -interface ReportingInternalSetup { +export interface ReportingInternalSetup { browserDriverFactory: HeadlessChromiumDriverFactory; elasticsearch: ElasticsearchServiceSetup; + licensing: LicensingPluginSetup; + basePath: BasePath['get']; + router: IRouter; + security: SecurityPluginSetup; } + interface ReportingInternalStart { enqueueJob: EnqueueJobFn; esqueue: ESQueueInstance; @@ -46,30 +48,10 @@ export class ReportingCore { private readonly pluginStart$ = new Rx.ReplaySubject(); private exportTypesRegistry = getExportTypesRegistry(); - constructor(private logger: LevelLogger, private config: ReportingConfig) {} - - legacySetup( - xpackMainPlugin: XPackMainPlugin, - reporting: ReportingPluginSpecOptions, - __LEGACY: ServerFacade, - plugins: ReportingSetupDeps - ) { - // legacy plugin status - mirrorPluginStatus(xpackMainPlugin, reporting); - - // legacy license check - const checkLicense = checkLicenseFactory(this.exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // legacy routes - registerRoutes(this, __LEGACY, plugins, this.logger); - } + constructor(private config: ReportingConfig) {} public pluginSetup(reportingSetupDeps: ReportingInternalSetup) { + this.pluginSetupDeps = reportingSetupDeps; this.pluginSetup$.next(reportingSetupDeps); } @@ -96,23 +78,35 @@ export class ReportingCore { return (await this.getPluginStartDeps()).enqueueJob; } - public getConfig() { + public async getLicenseInfo() { + const { licensing } = this.getPluginSetupDeps(); + return await licensing.license$ + .pipe( + map((license) => checkLicense(this.getExportTypesRegistry(), license)), + first() + ) + .toPromise(); + } + + public getConfig(): ReportingConfig { return this.config; } - public async getScreenshotsObservable() { - const { browserDriverFactory } = await this.getPluginSetupDeps(); + + public getScreenshotsObservable(): ScreenshotsObservableFn { + const { browserDriverFactory } = this.getPluginSetupDeps(); return screenshotsObservableFactory(this.config.get('capture'), browserDriverFactory); } + public getPluginSetupDeps() { + if (!this.pluginSetupDeps) { + throw new Error(`"pluginSetupDeps" dependencies haven't initialized yet`); + } + return this.pluginSetupDeps; + } + /* * Outside dependencies */ - private async getPluginSetupDeps() { - if (this.pluginSetupDeps) { - return this.pluginSetupDeps; - } - return await this.pluginSetup$.pipe(first()).toPromise(); - } private async getPluginStartDeps() { if (this.pluginStartDeps) { @@ -122,15 +116,15 @@ export class ReportingCore { } public async getElasticsearchService() { - return (await this.getPluginSetupDeps()).elasticsearch; + return this.getPluginSetupDeps().elasticsearch; } public async getSavedObjectsClient(fakeRequest: KibanaRequest) { const { savedObjects } = await this.getPluginStartDeps(); - return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClient; + return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClientContract; } - public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClient) { + public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClientContract) { const { uiSettings: uiSettingsService } = await this.getPluginStartDeps(); const scopedUiSettingsService = uiSettingsService.asScopedToClient(savedObjectsClient); return scopedUiSettingsService; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts index 37272325b97d0..14abd53cc83d9 100644 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -7,8 +7,8 @@ import { Legacy } from 'kibana'; import { take } from 'rxjs/operators'; import { PluginInitializerContext } from 'src/core/server'; -import { ReportingPluginSpecOptions } from '../'; import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; +import { ReportingPluginSpecOptions } from '../'; import { PluginsSetup } from '../../../../plugins/reporting/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { buildConfig } from './config'; @@ -42,6 +42,7 @@ export const legacyInit = async ( server.newPlatform.coreContext as PluginInitializerContext, buildConfig(coreSetup, server, reportingConfig) ); + await pluginInstance.setup(coreSetup, { elasticsearch: coreSetup.elasticsearch, licensing: server.newPlatform.setup.plugins.licensing as LicensingPluginSetup, diff --git a/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js deleted file mode 100644 index 294a0df56756e..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicenseFactory } from '../check_license'; - -describe('check_license', function () { - let mockLicenseInfo; - let checkLicense; - - beforeEach(() => { - mockLicenseInfo = {}; - checkLicense = checkLicenseFactory({ - getAll: () => [ - { - id: 'test', - name: 'Test Export Type', - jobType: 'testJobType', - }, - ], - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(false); - }); - - it('should set test.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(true); - }); - - it('should set test.enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(true); - }); - - it('should set management.jobTypes to contain testJobType', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.contain('testJobType'); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(false); - }); - - it('should set test.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => false)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(false); - }); - - it('should set test.showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(false); - }); - - it('should set management.jobTypes to an empty array', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be.an(Array); - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.have.length(0); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts b/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts new file mode 100644 index 0000000000000..366a8d94286f1 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { checkLicense } from './check_license'; +import { ILicense } from '../../../../../plugins/licensing/server'; +import { ExportTypesRegistry } from './export_types_registry'; + +describe('check_license', () => { + let exportTypesRegistry: ExportTypesRegistry; + let license: ILicense; + + beforeEach(() => { + exportTypesRegistry = ({ + getAll: () => [], + } as unknown) as ExportTypesRegistry; + }); + + describe('license information is not ready', () => { + beforeEach(() => { + exportTypesRegistry = ({ + getAll: () => [{ id: 'csv' }], + } as unknown) as ExportTypesRegistry; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.showLinks).toEqual(true); + }); + + it('should set csv.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, undefined).csv.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.enableLinks).toEqual(false); + }); + + it('should set csv.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, undefined).csv.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.jobTypes).toEqual(undefined); + }); + }); + + describe('license information is not available', () => { + beforeEach(() => { + license = { + type: undefined, + } as ILicense; + exportTypesRegistry = ({ + getAll: () => [{ id: 'csv' }], + } as unknown) as ExportTypesRegistry; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set csv.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).csv.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(false); + }); + + it('should set csv.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).csv.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + + describe('license information is available', () => { + beforeEach(() => { + license = {} as ILicense; + }); + + describe('& license is > basic', () => { + beforeEach(() => { + license.type = 'gold'; + exportTypesRegistry = ({ + getAll: () => [{ id: 'pdf', validLicenses: ['gold'], jobType: 'printable_pdf' }], + } as unknown) as ExportTypesRegistry; + }); + + describe('& license is active', () => { + beforeEach(() => (license.isActive = true)); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should setpdf.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(true); + }); + + it('should setpdf.enableLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.enableLinks).toEqual(true); + }); + + it('should set management.jobTypes to contain testJobType', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toContain( + 'printable_pdf' + ); + }); + }); + + describe('& license is expired', () => { + beforeEach(() => { + license.isActive = false; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set pdf.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(false); + }); + + it('should set pdf.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + }); + + describe('& license is basic', () => { + beforeEach(() => { + license.type = 'basic'; + exportTypesRegistry = ({ + getAll: () => [{ id: 'pdf', validLicenses: ['gold'], jobType: 'printable_pdf' }], + } as unknown) as ExportTypesRegistry; + }); + + describe('& license is active', () => { + beforeEach(() => { + license.isActive = true; + }); + + it('should set management.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(false); + }); + + it('should set test.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(false); + }); + + it('should set management.jobTypes to an empty array', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual([]); + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toHaveLength(0); + }); + }); + + describe('& license is expired', () => { + beforeEach(() => { + license.isActive = false; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set test.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts index 80cf315539441..1b4eeaa0bae3e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { XPackInfo } from '../../../xpack_main/server/lib/xpack_info'; -import { XPackInfoLicense } from '../../../xpack_main/server/lib/xpack_info_license'; +import { ILicense } from '../../../../../plugins/licensing/server'; import { ExportTypeDefinition } from '../types'; import { ExportTypesRegistry } from './export_types_registry'; -interface LicenseCheckResult { +export interface LicenseCheckResult { showLinks: boolean; enableLinks: boolean; message?: string; + jobTypes?: string[]; } const messages = { getUnavailable: () => { return 'You cannot use Reporting because license information is not available at this time.'; }, - getExpired: (license: XPackInfoLicense) => { - return `You cannot use Reporting because your ${license.getType()} license has expired.`; + getExpired: (license: ILicense) => { + return `You cannot use Reporting because your ${license.type} license has expired.`; }, }; @@ -29,8 +29,8 @@ const makeManagementFeature = ( ) => { return { id: 'management', - checkLicense: (license: XPackInfoLicense | null) => { - if (!license) { + checkLicense: (license?: ILicense) => { + if (!license || !license.type) { return { showLinks: true, enableLinks: false, @@ -38,7 +38,7 @@ const makeManagementFeature = ( }; } - if (!license.isActive()) { + if (!license.isActive) { return { showLinks: true, enableLinks: false, @@ -47,7 +47,7 @@ const makeManagementFeature = ( } const validJobTypes = exportTypes - .filter((exportType) => license.isOneOf(exportType.validLicenses)) + .filter((exportType) => exportType.validLicenses.includes(license.type || '')) .map((exportType) => exportType.jobType); return { @@ -64,8 +64,8 @@ const makeExportTypeFeature = ( ) => { return { id: exportType.id, - checkLicense: (license: XPackInfoLicense | null) => { - if (!license) { + checkLicense: (license?: ILicense) => { + if (!license || !license.type) { return { showLinks: true, enableLinks: false, @@ -73,17 +73,15 @@ const makeExportTypeFeature = ( }; } - if (!license.isOneOf(exportType.validLicenses)) { + if (!exportType.validLicenses.includes(license.type)) { return { showLinks: false, enableLinks: false, - message: `Your ${license.getType()} license does not support ${ - exportType.name - } Reporting. Please upgrade your license.`, + message: `Your ${license.type} license does not support ${exportType.name} Reporting. Please upgrade your license.`, }; } - if (!license.isActive()) { + if (!license.isActive) { return { showLinks: true, enableLinks: false, @@ -99,18 +97,18 @@ const makeExportTypeFeature = ( }; }; -export function checkLicenseFactory(exportTypesRegistry: ExportTypesRegistry) { - return function checkLicense(xpackInfo: XPackInfo) { - const license = xpackInfo === null || !xpackInfo.isAvailable() ? null : xpackInfo.license; - const exportTypes = Array.from(exportTypesRegistry.getAll()); - const reportingFeatures = [ - ...exportTypes.map(makeExportTypeFeature), - makeManagementFeature(exportTypes), - ]; +export function checkLicense( + exportTypesRegistry: ExportTypesRegistry, + license: ILicense | undefined +) { + const exportTypes = Array.from(exportTypesRegistry.getAll()); + const reportingFeatures = [ + ...exportTypes.map(makeExportTypeFeature), + makeManagementFeature(exportTypes), + ]; - return reportingFeatures.reduce((result, feature) => { - result[feature.id] = feature.checkLicense(license); - return result; - }, {} as Record); - }; + return reportingFeatures.reduce((result, feature) => { + result[feature.id] = feature.checkLicense(license); + return result; + }, {} as Record); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 8ffb99f7a14c8..6367c8a1da98a 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,8 +5,9 @@ */ import { EventEmitter } from 'events'; -import { get } from 'lodash'; -import { ConditionalHeaders, ESQueueCreateJobFn, RequestFacade } from '../../server/types'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../../../plugins/security/server'; +import { ESQueueCreateJobFn } from '../../server/types'; import { ReportingCore } from '../core'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; @@ -29,9 +30,9 @@ export type Job = EventEmitter & { export type EnqueueJobFn = ( exportTypeId: string, jobParams: JobParamsType, - user: string, - headers: Record, - request: RequestFacade + user: AuthenticatedUser | null, + context: RequestHandlerContext, + request: KibanaRequest ) => Promise; export function enqueueJobFactory( @@ -42,18 +43,17 @@ export function enqueueJobFactory( const queueTimeout = config.get('queue', 'timeout'); const browserType = config.get('capture', 'browser', 'type'); const maxAttempts = config.get('capture', 'maxAttempts'); - const logger = parentLogger.clone(['queue-job']); return async function enqueueJob( exportTypeId: string, jobParams: JobParamsType, - user: string, - headers: ConditionalHeaders['headers'], - request: RequestFacade + user: AuthenticatedUser | null, + context: RequestHandlerContext, + request: KibanaRequest ): Promise { type CreateJobFn = ESQueueCreateJobFn; - + const username = user ? user.username : false; const esqueue = await reporting.getEsqueue(); const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); @@ -62,11 +62,11 @@ export function enqueueJobFactory( } const createJob = exportType.createJobFactory(reporting, logger) as CreateJobFn; - const payload = await createJob(jobParams, headers, request); + const payload = await createJob(jobParams, context, request); const options = { timeout: queueTimeout, - created_by: get(user, 'username', false), + created_by: username, browser_type: browserType, max_attempts: maxAttempts, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts index 8e8b1c83d5a40..164ffc5742d04 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts @@ -4,16 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { SecurityPluginSetup } from '../../../../../plugins/security/server'; import { KibanaRequest } from '../../../../../../src/core/server'; -import { ReportingSetupDeps } from '../types'; -import { LevelLogger } from './level_logger'; -export function getUserFactory(security: ReportingSetupDeps['security'], logger: LevelLogger) { - /* - * Legacy.Request because this is called from routing middleware - */ - return async (request: Legacy.Request) => { - return security?.authc.getCurrentUser(KibanaRequest.from(request)) ?? null; +export function getUserFactory(security?: SecurityPluginSetup) { + return (request: KibanaRequest) => { + return security?.authc.getCurrentUser(request) ?? null; }; } diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index 2a8fa45b6fcef..0e9c49b170887 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -5,7 +5,7 @@ */ export { LevelLogger } from './level_logger'; -export { checkLicenseFactory } from './check_license'; +export { checkLicense } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 1abf58c29b481..5153fd0f4e5b8 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; +import { AuthenticatedUser } from '../../../../../plugins/security/server'; import { ReportingConfig } from '../'; import { JobSource } from '../types'; @@ -40,6 +40,8 @@ interface CountAggResult { count: number; } +const getUsername = (user: AuthenticatedUser | null) => (user ? user.username : false); + export function jobsQueryFactory( config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup @@ -47,10 +49,6 @@ export function jobsQueryFactory( const index = config.get('index'); const { callAsInternalUser } = elasticsearch.adminClient; - function getUsername(user: any) { - return get(user, 'username', false); - } - function execQuery(queryType: string, body: QueryBody) { const defaultBody: Record = { search: { @@ -82,9 +80,14 @@ export function jobsQueryFactory( } return { - list(jobTypes: string[], user: any, page = 0, size = defaultSize, jobIds: string[] | null) { + list( + jobTypes: string[], + user: AuthenticatedUser | null, + page = 0, + size = defaultSize, + jobIds: string[] | null + ) { const username = getUsername(user); - const body: QueryBody = { size, from: size * page, @@ -108,9 +111,8 @@ export function jobsQueryFactory( return getHits(execQuery('search', body)); }, - count(jobTypes: string[], user: any) { + count(jobTypes: string[], user: AuthenticatedUser | null) { const username = getUsername(user); - const body: QueryBody = { query: { constant_score: { @@ -129,9 +131,12 @@ export function jobsQueryFactory( }); }, - get(user: any, id: string, opts: GetOpts = {}): Promise | void> { + get( + user: AuthenticatedUser | null, + id: string, + opts: GetOpts = {} + ): Promise | void> { if (!id) return Promise.resolve(); - const username = getUsername(user); const body: QueryBody = { @@ -164,14 +169,12 @@ export function jobsQueryFactory( const query = { id, index: deleteIndex }; return callAsInternalUser('delete', query); } catch (error) { - const wrappedError = new Error( + throw new Error( i18n.translate('xpack.reporting.jobsQuery.deleteError', { defaultMessage: 'Could not delete the report: {error}', values: { error: error.message }, }) ); - - throw Boom.boomify(wrappedError, { statusCode: error.status }); } }, }; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index 78c2ce5b9b106..5a407ad3e4c4a 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -8,10 +8,13 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core import { createBrowserDriverFactory } from './browsers'; import { ReportingConfig } from './config'; import { ReportingCore } from './core'; +import { registerRoutes } from './routes'; import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; import { registerReportingUsageCollector } from './usage'; +// @ts-ignore no module definition +import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; export class ReportingPlugin implements Plugin { @@ -22,24 +25,34 @@ export class ReportingPlugin constructor(context: PluginInitializerContext, config: ReportingConfig) { this.config = config; this.logger = new LevelLogger(context.logger.get('reporting')); - this.reportingCore = new ReportingCore(this.logger, this.config); + this.reportingCore = new ReportingCore(this.config); } public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { const { config } = this; - const { elasticsearch, __LEGACY } = plugins; + const { elasticsearch, __LEGACY, licensing, security } = plugins; + const router = core.http.createRouter(); + const basePath = core.http.basePath.get; + const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); // required for validations :( - runValidations(config, elasticsearch, browserDriverFactory, this.logger); + // legacy plugin status + mirrorPluginStatus(xpackMainLegacy, reportingLegacy); - const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - this.reportingCore.legacySetup(xpackMainLegacy, reportingLegacy, __LEGACY, plugins); + const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); + const deps = { + browserDriverFactory, + elasticsearch, + licensing, + basePath, + router, + security, + }; - // Register a function with server to manage the collection of usage stats - registerReportingUsageCollector(this.reportingCore, plugins); + runValidations(config, elasticsearch, browserDriverFactory, this.logger); - // regsister setup internals - this.reportingCore.pluginSetup({ browserDriverFactory, elasticsearch }); + this.reportingCore.pluginSetup(deps); + registerReportingUsageCollector(this.reportingCore, plugins); + registerRoutes(this.reportingCore, this.logger); return {}; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 3f79d51382a81..2a12a64d67a35 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -4,73 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; -import Joi from 'joi'; -import { Legacy } from 'kibana'; import rison from 'rison-node'; +import { schema } from '@kbn/config-schema'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { HandlerErrorFunction, HandlerFunction } from './types'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { - GetRouteConfigFactoryFn, - getRouteConfigFactoryReportingPre, - RouteConfigFactory, -} from './lib/route_config_factories'; -import { HandlerErrorFunction, HandlerFunction, ReportingResponseToolkit } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; export function registerGenerateFromJobParams( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, handler: HandlerFunction, - handleError: HandlerErrorFunction, - logger: Logger + handleError: HandlerErrorFunction ) { - const config = reporting.getConfig(); - const getRouteConfig = () => { - const getOriginalRouteConfig: GetRouteConfigFactoryFn = getRouteConfigFactoryReportingPre( - config, - plugins, - logger - ); - const routeConfigFactory: RouteConfigFactory = getOriginalRouteConfig( - ({ params: { exportType } }) => exportType - ); + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; - return { - ...routeConfigFactory, + router.post( + { + path: `${BASE_GENERATE}/{exportType}`, validate: { - params: Joi.object({ - exportType: Joi.string().required(), - }).required(), - payload: Joi.object({ - jobParams: Joi.string().optional().default(null), - }).allow(null), // allow optional payload - query: Joi.object({ - jobParams: Joi.string().default(null), - }).default(), + params: schema.object({ + exportType: schema.string({ minLength: 2 }), + }), + body: schema.nullable( + schema.object({ + jobParams: schema.maybe(schema.string()), + }) + ), + query: schema.nullable( + schema.object({ + jobParams: schema.string({ + defaultValue: '', + }), + }) + ), }, - }; - }; - - // generate report - server.route({ - path: `${BASE_GENERATE}/{exportType}`, - method: 'POST', - options: getRouteConfig(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); + }, + userHandler(async (user, context, req, res) => { let jobParamsRison: string | null; - if (request.payload) { - const { jobParams: jobParamsPayload } = request.payload as { jobParams: string }; + if (req.body) { + const { jobParams: jobParamsPayload } = req.body as { jobParams: string }; jobParamsRison = jobParamsPayload; } else { - const { jobParams: queryJobParams } = request.query as { jobParams: string }; + const { jobParams: queryJobParams } = req.query as { jobParams: string }; if (queryJobParams) { jobParamsRison = queryJobParams; } else { @@ -79,37 +59,46 @@ export function registerGenerateFromJobParams( } if (!jobParamsRison) { - throw boom.badRequest('A jobParams RISON string is required'); + return res.customError({ + statusCode: 400, + body: 'A jobParams RISON string is required in the querystring or POST body', + }); } - const { exportType } = request.params; + const { exportType } = req.params as { exportType: string }; let jobParams; - let response; + try { jobParams = rison.decode(jobParamsRison) as object | null; if (!jobParams) { - throw new Error('missing jobParams!'); + return res.customError({ + statusCode: 400, + body: 'Missing jobParams!', + }); } } catch (err) { - throw boom.badRequest(`invalid rison: ${jobParamsRison}`); + return res.customError({ + statusCode: 400, + body: `invalid rison: ${jobParamsRison}`, + }); } + try { - response = await handler(exportType, jobParams, legacyRequest, h); + return await handler(user, exportType, jobParams, context, req, res); } catch (err) { - throw handleError(exportType, err); + return handleError(res, err); } - return response; - }, - }); + }) + ); // Get route to generation endpoint: show error about GET method to user - server.route({ - path: `${BASE_GENERATE}/{p*}`, - method: 'GET', - handler: () => { - const err = boom.methodNotAllowed('GET is not allowed'); - err.output.headers.allow = 'POST'; - return err; + router.get( + { + path: `${BASE_GENERATE}/{p*}`, + validate: false, }, - }); + (context, req, res) => { + return res.customError({ statusCode: 405, body: 'GET is not allowed' }); + } + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts index 03a893d1abeb4..4bc143b911572 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; +import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; -import { - HandlerErrorFunction, - HandlerFunction, - QueuedJobPayload, - ReportingResponseToolkit, -} from './types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* * This function registers API Endpoints for queuing Reporting jobs. The API inputs are: @@ -31,22 +23,31 @@ import { */ export function registerGenerateCsvFromSavedObject( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, handleRoute: HandlerFunction, - handleRouteError: HandlerErrorFunction, - logger: Logger + handleRouteError: HandlerErrorFunction ) { - const config = reporting.getConfig(); - const routeOptions = getRouteOptionsCsv(config, plugins, logger); - - server.route({ - path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, - method: 'POST', - options: routeOptions, - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const requestFacade = makeRequestFacade(legacyRequest); - + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; + router.post( + { + path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, + validate: { + params: schema.object({ + savedObjectType: schema.string({ minLength: 2 }), + savedObjectId: schema.string({ minLength: 2 }), + }), + body: schema.object({ + state: schema.object({}), + timerange: schema.object({ + timezone: schema.string({ defaultValue: 'UTC' }), + min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + }), + }), + }, + }, + userHandler(async (user, context, req, res) => { /* * 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle * 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params @@ -54,19 +55,31 @@ export function registerGenerateCsvFromSavedObject( */ let result: QueuedJobPayload; try { - const jobParams = getJobParamsFromRequest(requestFacade, { isImmediate: false }); - result = await handleRoute(CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobParams, legacyRequest, h); // pass the original request because the handler will make the request facade on its own + const jobParams = getJobParamsFromRequest(req, { isImmediate: false }); + result = await handleRoute( + user, + CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobParams, + context, + req, + res + ); } catch (err) { - throw handleRouteError(CSV_FROM_SAVEDOBJECT_JOB_TYPE, err); + return handleRouteError(res, err); } if (get(result, 'source.job') == null) { - throw new Error( - `The Export handler is expected to return a result with job info! ${result}` - ); + return res.badRequest({ + body: `The Export handler is expected to return a result with job info! ${result}`, + }); } - return result; - }, - }); + return res.ok({ + body: result, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 22aebb05cdf49..8a6d4553dfa9c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -4,22 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseObject } from 'hapi'; -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; +import { HandlerErrorFunction } from './types'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFactory, executeJobFactory } from '../../export_types/csv_from_savedobject'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; -import { JobDocOutput, ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; -import { ReportingResponseToolkit } from './types'; - -type ResponseFacade = ResponseObject & { - isBoom: boolean; -}; +import { JobDocOutput } from '../types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: @@ -32,61 +26,77 @@ type ResponseFacade = ResponseObject & { */ export function registerGenerateCsvFromSavedObjectImmediate( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, + handleError: HandlerErrorFunction, parentLogger: Logger ) { - const config = reporting.getConfig(); - const routeOptions = getRouteOptionsCsv(config, plugins, parentLogger); + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: * - re-use the createJob function to build up es query config * - re-use the executeJob function to run the scan and scroll queries and capture the entire CSV in a result object. */ - server.route({ - path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, - method: 'POST', - options: routeOptions, - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); + router.post( + { + path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, + validate: { + params: schema.object({ + savedObjectType: schema.string({ minLength: 5 }), + savedObjectId: schema.string({ minLength: 5 }), + }), + body: schema.object({ + state: schema.object({}, { unknowns: 'allow' }), + timerange: schema.object({ + timezone: schema.string({ defaultValue: 'UTC' }), + min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + }), + }), + }, + }, + userHandler(async (user, context, req, res) => { const logger = parentLogger.clone(['savedobject-csv']); - const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); + const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); const createJobFn = createJobFactory(reporting, logger); const executeJobFn = await executeJobFactory(reporting, logger); // FIXME: does not "need" to be async - const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( - jobParams, - request.headers, - request - ); - const { - content_type: jobOutputContentType, - content: jobOutputContent, - size: jobOutputSize, - }: JobDocOutput = await executeJobFn(null, jobDocPayload, request); - logger.info(`Job output size: ${jobOutputSize} bytes`); + try { + const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( + jobParams, + req.headers, + context, + req + ); + const { + content_type: jobOutputContentType, + content: jobOutputContent, + size: jobOutputSize, + }: JobDocOutput = await executeJobFn(null, jobDocPayload, context, req); - /* - * ESQueue worker function defaults `content` to null, even if the - * executeJob returned undefined. - * - * This converts null to undefined so the value can be sent to h.response() - */ - if (jobOutputContent === null) { - logger.warn('CSV Job Execution created empty content result'); - } - const response = h - .response(jobOutputContent ? jobOutputContent : undefined) - .type(jobOutputContentType); + logger.info(`Job output size: ${jobOutputSize} bytes`); - // Set header for buffer download, not streaming - const { isBoom } = response as ResponseFacade; - if (isBoom == null) { - response.header('accept-ranges', 'none'); - } + /* + * ESQueue worker function defaults `content` to null, even if the + * executeJob returned undefined. + * + * This converts null to undefined so the value can be sent to h.response() + */ + if (jobOutputContent === null) { + logger.warn('CSV Job Execution created empty content result'); + } - return response; - }, - }); + return res.ok({ + body: jobOutputContent || '', + headers: { + 'content-type': jobOutputContentType, + 'accept-ranges': 'none', + }, + }); + } catch (err) { + return handleError(res, err); + } + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts index d767d37a477ab..fdde3253cf28e 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -4,140 +4,162 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ReportingConfig, ReportingCore } from '../'; -import { createMockReportingCore } from '../../test_helpers'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; import { registerJobGenerationRoutes } from './generation'; +import { createMockReportingCore } from '../../test_helpers'; +import { ReportingCore } from '..'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; +import { ExportTypeDefinition } from '../types'; +import { LevelLogger } from '../lib'; +import { of } from 'rxjs'; + +type setupServerReturn = UnwrapPromise>; + +describe('POST /api/reporting/generate', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + + const config = { + get: jest.fn().mockImplementation((...args) => { + const key = args.join('.'); + switch (key) { + case 'queue.indexInterval': + return 10000; + case 'queue.timeout': + return 10000; + case 'index': + return '.reporting'; + case 'queue.pollEnabled': + return false; + default: + return; + } + }), + kbnConfig: { get: jest.fn() }, + }; + const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + const mockDeps = ({ + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['superuser'], + username: 'Tom Riddle', + }), + }, + }, + router: httpSetup.createRouter(''), + licensing: { + license$: of({ + isActive: true, + isAvailable: true, + type: 'gold', + }), + }, + } as unknown) as any; + core = await createMockReportingCore(config, mockDeps); + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'printablePdf', + jobType: 'printable_pdf', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + core.getExportTypesRegistry = () => exportTypesRegistry; + }); -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -let mockServer: Hapi.Server; -let mockReportingPlugin: ReportingCore; -let mockReportingConfig: ReportingConfig; - -const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), -} as unknown) as Logger; - -beforeEach(async () => { - mockServer = new Hapi.Server({ - debug: false, - port: 8080, - routes: { log: { collect: true } }, + afterEach(async () => { + mockLogger.debug.mockReset(); + mockLogger.error.mockReset(); + await server.stop(); }); - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); -}); + it('returns 400 if there are no job params', async () => { + registerJobGenerationRoutes(core, mockLogger); -const mockPlugins = { - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -}; - -const getErrorsFromRequest = (request: Hapi.Request) => { - // @ts-ignore error property doesn't exist on RequestLog - return request.logs.filter((log) => log.tags.includes('error')).map((log) => log.error); // NOTE: error stack is available -}; - -test(`returns 400 if there are no job params`, async () => { - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - }; + await server.start(); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: A jobParams RISON string is required], - ] - `); -}); + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"A jobParams RISON string is required in the querystring or POST body"' + ) + ); + }); -test(`returns 400 if job params is invalid`, async () => { - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - payload: { jobParams: `foo:` }, - }; + it('returns 400 if job params query is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: invalid rison: foo:], - ] - `); -}); + await server.start(); -test(`returns 500 if job handler throws an error`, async () => { - mockReportingPlugin.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ - toJSON: () => { - throw new Error('you found me'); - }, - })); - - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - payload: { jobParams: `abc` }, - }; + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf?jobParams=foo:') + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 if job params body is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `foo:` }) + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 export type is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: you found me], - [Error: you found me], - ] - `); + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/TonyHawksProSkater2') + .send({ jobParams: `abc` }) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"') + ); + }); + + it('returns 400 if job handler throws an error', async () => { + const errorText = 'you found me'; + core.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ + toJSON: () => { + throw new Error(errorText); + }, + })); + + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `abc` }) + .expect(400) + .then(({ body }) => { + expect(body.message).toMatchInlineSnapshot(`"${errorText}"`); + }); + }); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 56faa37d5fcbd..f2e616c0803a7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; +import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; -import { Legacy } from 'kibana'; +import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { ReportingResponseToolkit } from './types'; +import { HandlerFunction } from './types'; const esErrors = elasticsearchErrors as Record; -export function registerJobGenerationRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { const config = reporting.getConfig(); const downloadBaseUrl = config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; @@ -32,48 +25,71 @@ export function registerJobGenerationRoutes( /* * Generates enqueued job details to use in responses */ - async function handler( - exportTypeId: string, - jobParams: object, - legacyRequest: Legacy.Request, - h: ReportingResponseToolkit - ) { - const request = makeRequestFacade(legacyRequest); - const user = request.pre.user; - const headers = request.headers; + const handler: HandlerFunction = async (user, exportTypeId, jobParams, context, req, res) => { + const licenseInfo = await reporting.getLicenseInfo(); + const licenseResults = licenseInfo[exportTypeId]; + + if (!licenseResults) { + return res.badRequest({ body: `Invalid export-type of ${exportTypeId}` }); + } + + if (!licenseResults.enableLinks) { + return res.forbidden({ body: licenseResults.message }); + } const enqueueJob = await reporting.getEnqueueJob(); - const job = await enqueueJob(exportTypeId, jobParams, user, headers, request); + const job = await enqueueJob(exportTypeId, jobParams, user, context, req); // return the queue's job information const jobJson = job.toJSON(); - return h - .response({ + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: { path: `${downloadBaseUrl}/${jobJson.id}`, job: jobJson, - }) - .type('application/json'); - } + }, + }); + }; + + function handleError(res: typeof kibanaResponseFactory, err: Error | Boom) { + if (err instanceof Boom) { + return res.customError({ + statusCode: err.output.statusCode, + body: err.output.payload.message, + }); + } - function handleError(exportTypeId: string, err: Error) { if (err instanceof esErrors['401']) { - return boom.unauthorized(`Sorry, you aren't authenticated`); + return res.unauthorized({ + body: `Sorry, you aren't authenticated`, + }); } + if (err instanceof esErrors['403']) { - return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); + return res.forbidden({ + body: `Sorry, you are not authorized`, + }); } + if (err instanceof esErrors['404']) { - return boom.boomify(err, { statusCode: 404 }); + return res.notFound({ + body: err.message, + }); } - return err; + + return res.badRequest({ + body: err.message, + }); } - registerGenerateFromJobParams(reporting, server, plugins, handler, handleError, logger); + registerGenerateFromJobParams(reporting, handler, handleError); // Register beta panel-action download-related API's if (config.get('csv', 'enablePanelActionDownload')) { - registerGenerateCsvFromSavedObject(reporting, server, plugins, handler, handleError, logger); - registerGenerateCsvFromSavedObjectImmediate(reporting, server, plugins, logger); + registerGenerateCsvFromSavedObject(reporting, handler, handleError); + registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger); } } diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/legacy/plugins/reporting/server/routes/index.ts index 556f4e12b077e..005d82086665c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/index.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/index.ts @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../'; import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; import { registerJobGenerationRoutes } from './generation'; import { registerJobInfoRoutes } from './jobs'; +import { ReportingCore } from '../core'; -export function registerRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { - registerJobGenerationRoutes(reporting, server, plugins, logger); - registerJobInfoRoutes(reporting, server, plugins, logger); +export function registerRoutes(reporting: ReportingCore, logger: Logger) { + registerJobGenerationRoutes(reporting, logger); + registerJobInfoRoutes(reporting); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts index 4f597bcee858e..73f3c660141c1 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts @@ -4,327 +4,347 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ReportingConfig, ReportingCore } from '../'; -import { LevelLogger } from '../lib'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; +import { registerJobInfoRoutes } from './jobs'; import { createMockReportingCore } from '../../test_helpers'; +import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { ExportTypeDefinition, ReportingSetupDeps } from '../types'; -import { registerJobInfoRoutes } from './jobs'; - -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -let mockServer: any; -let exportTypesRegistry: ExportTypesRegistry; -let mockReportingPlugin: ReportingCore; -let mockReportingConfig: ReportingConfig; -const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), -} as unknown) as LevelLogger; - -beforeEach(async () => { - mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); - exportTypesRegistry = new ExportTypesRegistry(); - exportTypesRegistry.register({ - id: 'unencoded', - jobType: 'unencodedJobType', - jobContentExtension: 'csv', - } as ExportTypeDefinition); - exportTypesRegistry.register({ - id: 'base64Encoded', - jobType: 'base64EncodedJobType', - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - } as ExportTypeDefinition); - - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getExportTypesRegistry = () => exportTypesRegistry; -}); - -const mockPlugins = ({ - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -} as unknown) as ReportingSetupDeps; - -const getHits = (...sources: any) => { - return { - hits: { - hits: sources.map((source: object) => ({ _source: source })), - }, - }; -}; - -const getErrorsFromRequest = (request: any) => - request.logs.filter((log: any) => log.tags.includes('error')).map((log: any) => log.error); - -test(`returns 404 if job not found`, async () => { - // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - const response = await mockServer.inject(request); - const { statusCode } = response; - expect(statusCode).toBe(404); -}); - -test(`returns 401 if not valid job type`, async () => { - // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', +import { ExportTypeDefinition } from '../types'; +import { LevelLogger } from '../lib'; +import { ReportingInternalSetup } from '../core'; +import { of } from 'rxjs'; + +type setupServerReturn = UnwrapPromise>; + +describe('GET /api/reporting/jobs/download', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + + const config = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; + const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + const getHits = (...sources: any) => { + return { + hits: { + hits: sources.map((source: object) => ({ _source: source })), + }, + }; }; - const { statusCode } = await mockServer.inject(request); - expect(statusCode).toBe(401); -}); - -describe(`when job is incomplete`, () => { - const getIncompleteResponse = async () => { + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + core = await createMockReportingCore(config, ({ + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['superuser'], + username: 'Tom Riddle', + }), + }, + }, + router: httpSetup.createRouter(''), + licensing: { + license$: of({ + isActive: true, + isAvailable: true, + type: 'gold', + }), + }, + } as unknown) as ReportingInternalSetup); // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ), - }; + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'unencoded', + jobType: 'unencodedJobType', + jobContentExtension: 'csv', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + exportTypesRegistry.register({ + id: 'base64Encoded', + jobType: 'base64EncodedJobType', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + core.getExportTypesRegistry = () => exportTypesRegistry; + }); - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + afterEach(async () => { + mockLogger.debug.mockReset(); + mockLogger.error.mockReset(); + await server.stop(); + }); - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', + it('fails on malformed download IDs', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; + registerJobInfoRoutes(core); - return await mockServer.inject(request); - }; + await server.start(); - test(`sets statusCode to 503`, async () => { - const { statusCode } = await getIncompleteResponse(); - expect(statusCode).toBe(503); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/1') + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"[request params.docId]: value has length [1] but it must have a minimum length of [3]."' + ) + ); }); - test(`uses status as payload`, async () => { - const { payload } = await getIncompleteResponse(); - expect(payload).toBe('pending'); - }); + it('fails on unauthenticated users', async () => { + // @ts-ignore + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => undefined, + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); + await server.start(); - test(`sets retry-after header to 30`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['retry-after']).toBe(30); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(401) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`) + ); }); -}); -describe(`when job is failed`, () => { - const getFailedResponse = async () => { - const hits = getHits({ - jobtype: 'unencodedJobType', - status: 'failed', - output: { content: 'job failure message' }, - }); + it('fails on users without the appropriate role', async () => { // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; - - test(`sets status code to 500`, async () => { - const { statusCode } = await getFailedResponse(); - expect(statusCode).toBe(500); - }); + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['peasant'], + username: 'Tom Riddle', + }), + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getFailedResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); + await server.start(); - test(`sets the payload.reason to the job content`, async () => { - const { payload } = await getFailedResponse(); - expect(JSON.parse(payload).reason).toBe('job failure message'); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(403) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you don't have access to Reporting"`) + ); }); -}); -describe(`when job is completed`, () => { - const getCompletedResponse = async ({ - jobType = 'unencodedJobType', - outputContent = 'job output content', - outputContentType = 'application/pdf', - title = '', - } = {}) => { - const hits = getHits({ - jobtype: jobType, - status: 'completed', - output: { content: outputContent, content_type: outputContentType }, - payload: { - title, - }, - }); + it('returns 404 if job not found', async () => { // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + registerJobInfoRoutes(core); - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; + await server.start(); - test(`sets statusCode to 200`, async () => { - const { statusCode, request } = await getCompletedResponse(); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(statusCode).toBe(200); + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(404); }); - test(`doesn't encode output content for not-specified jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'unencodedJobType', - outputContent: 'test', - }); + it('returns a 401 if not a valid job type', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; + registerJobInfoRoutes(core); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + await server.start(); - expect(payload).toBe('test'); + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(401); }); - test(`base64 encodes output content for configured jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'base64EncodedJobType', - outputContent: 'test', - }); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + it('when a job is incomplete', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(503) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Retry-After', '30') + .then(({ text }) => expect(text).toEqual('pending')); + }); - expect(payload).toBe(Buffer.from('test', 'base64').toString()); + it('when a job fails', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue( + Promise.resolve( + getHits({ + jobtype: 'unencodedJobType', + status: 'failed', + output: { content: 'job failure message' }, + }) + ) + ), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(500) + .expect('Content-Type', 'application/json; charset=utf-8') + .then(({ body }) => + expect(body.message).toEqual('Reporting generation failed: job failure message') + ); }); - test(`specifies text/csv; charset=utf-8 contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ outputContentType: 'text/csv' }); + describe('successful downloads', () => { + const getCompleteHits = async ({ + jobType = 'unencodedJobType', + outputContent = 'job output content', + outputContentType = 'text/plain', + title = '', + } = {}) => { + return getHits({ + jobtype: jobType, + status: 'completed', + output: { content: outputContent, content_type: outputContentType }, + payload: { + title, + }, + }); + }; - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + it('when a known job-type is complete', async () => { + const hits = getCompleteHits(); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('content-disposition', 'inline; filename="report.csv"'); + }); - expect(headers['content-type']).toBe('text/csv; charset=utf-8'); - }); + it('succeeds when security is not there or disabled', async () => { + const hits = getCompleteHits(); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; - test(`specifies default filename in content-disposition header if no title`, async () => { - const { headers, request } = await getCompletedResponse({}); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.csv"'); - }); + // @ts-ignore + core.pluginSetupDeps.security = null; - test(`specifies payload title in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ title: 'something' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="something.csv"'); - }); + registerJobInfoRoutes(core); - test(`specifies jobContentExtension in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ jobType: 'base64EncodedJobType' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.pdf"'); - }); + await server.start(); - test(`specifies application/pdf contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ - outputContentType: 'application/pdf', + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('content-disposition', 'inline; filename="report.csv"'); }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-type']).toBe('application/pdf'); - }); - describe(`when non-whitelisted contentType specified in job output`, () => { - test(`sets statusCode to 500`, async () => { - const { statusCode, request } = await getCompletedResponse({ - outputContentType: 'application/html', + it(`doesn't encode output-content for non-specified job-types`, async () => { + const hits = getCompleteHits({ + jobType: 'unencodedJobType', + outputContent: 'test', }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); - expect(statusCode).toBe(500); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .then(({ text }) => expect(text).toEqual('test')); }); - test(`doesn't include job output content in payload`, async () => { - const { payload, request } = await getCompletedResponse({ - outputContentType: 'application/html', + it(`base64 encodes output content for configured jobTypes`, async () => { + const hits = getCompleteHits({ + jobType: 'base64EncodedJobType', + outputContent: 'test', + outputContentType: 'application/pdf', }); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'application/pdf') + .expect('content-disposition', 'inline; filename="report.pdf"') + .then(({ body }) => expect(Buffer.from(body).toString('base64')).toEqual('test')); }); - test(`logs error message about invalid content type`, async () => { - const { request } = await getCompletedResponse({ outputContentType: 'application/html' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); + it('refuses to return unknown content-types', async () => { + const hits = getCompleteHits({ + jobType: 'unencodedJobType', + outputContent: 'alert("all your base mine now");', + outputContentType: 'application/html', + }); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(400) + .then(({ body }) => { + expect(body).toEqual({ + error: 'Bad Request', + message: 'Unsupported content-type of application/html specified by job output', + statusCode: 400, + }); + }); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index 59090961998af..8c35f79ec0fb4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -5,24 +5,15 @@ */ import Boom from 'boom'; -import { ResponseObject } from 'hapi'; -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; import { jobsQueryFactory } from '../lib/jobs_query'; -import { JobDocOutput, JobSource, ReportingSetupDeps, ServerFacade } from '../types'; import { deleteJobResponseHandlerFactory, downloadJobResponseHandlerFactory, } from './lib/job_response_handler'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { - getRouteConfigFactoryDeletePre, - getRouteConfigFactoryDownloadPre, - getRouteConfigFactoryManagementPre, -} from './lib/route_config_factories'; -import { ReportingResponseToolkit } from './types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; interface ListQuery { page: string; @@ -31,193 +22,192 @@ interface ListQuery { } const MAIN_ENTRY = `${API_BASE_URL}/jobs`; -function isResponse(response: Boom | ResponseObject): response is ResponseObject { - return !(response as Boom).isBoom; -} - -export function registerJobInfoRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { +export async function registerJobInfoRoutes(reporting: ReportingCore) { const config = reporting.getConfig(); - const { elasticsearch } = plugins; + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { elasticsearch, router } = setupDeps; const jobsQuery = jobsQueryFactory(config, elasticsearch); - const getRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); // list jobs in the queue, paginated - server.route({ - path: `${MAIN_ENTRY}/list`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { page: queryPage, size: querySize, ids: queryIds } = request.query as ListQuery; + router.get( + { + path: `${MAIN_ENTRY}/list`, + validate: false, + }, + userHandler(async (user, context, req, res) => { + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + const { + page: queryPage = '0', + size: querySize = '10', + ids: queryIds = null, + } = req.query as ListQuery; const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; + const results = await jobsQuery.list(jobTypes, user, page, size, jobIds); - const results = jobsQuery.list( - request.pre.management.jobTypes, - request.pre.user, - page, - size, - jobIds - ); - return results; - }, - }); + return res.ok({ + body: results, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); // return the count of all jobs in the queue - server.route({ - path: `${MAIN_ENTRY}/count`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const results = jobsQuery.count(request.pre.management.jobTypes, request.pre.user); - return results; + router.get( + { + path: `${MAIN_ENTRY}/count`, + validate: false, }, - }); + userHandler(async (user, context, req, res) => { + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const count = await jobsQuery.count(jobTypes, user); + + return res.ok({ + body: count.toString(), + headers: { + 'content-type': 'text/plain', + }, + }); + }) + ); // return the raw output from a job - server.route({ - path: `${MAIN_ENTRY}/output/{docId}`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - return jobsQuery.get(request.pre.user, docId, { includeContent: true }).then( - (result): JobDocOutput => { - if (!result) { - throw Boom.notFound(); - } - const { - _source: { jobtype: jobType, output: jobOutput }, - } = result; - - if (!request.pre.management.jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } - - return jobOutput; - } - ); + router.get( + { + path: `${MAIN_ENTRY}/output/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 2 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const result = await jobsQuery.get(user, docId, { includeContent: true }); + + if (!result) { + throw Boom.notFound(); + } + + const { + _source: { jobtype: jobType, output: jobOutput }, + } = result; + + if (!jobTypes.includes(jobType)) { + throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); + } + + return res.ok({ + body: jobOutput, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); // return some info about the job - server.route({ - path: `${MAIN_ENTRY}/info/{docId}`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - return jobsQuery.get(request.pre.user, docId).then((result): JobSource['_source'] => { - if (!result) { - throw Boom.notFound(); - } - - const { _source: job } = result; - const { jobtype: jobType, payload: jobPayload } = job; - if (!request.pre.management.jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); - } - - return { + router.get( + { + path: `${MAIN_ENTRY}/info/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 2 }), + }), + }, + }, + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const result = await jobsQuery.get(user, docId); + + if (!result) { + throw Boom.notFound(); + } + + const { _source: job } = result; + const { jobtype: jobType, payload: jobPayload } = job; + + if (!jobTypes.includes(jobType)) { + throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); + } + + return res.ok({ + body: { ...job, payload: { ...jobPayload, headers: undefined, }, - }; + }, + headers: { + 'content-type': 'application/json', + }, }); - }, - }); + }) + ); // trigger a download of the output from a job const exportTypesRegistry = reporting.getExportTypesRegistry(); - const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(config, plugins, logger); - const downloadResponseHandler = downloadJobResponseHandlerFactory(config, elasticsearch, exportTypesRegistry); // prettier-ignore - server.route({ - path: `${MAIN_ENTRY}/download/{docId}`, - method: 'GET', - options: getRouteConfigDownload(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - let response = await downloadResponseHandler( - request.pre.management.jobTypes, - request.pre.user, - h, - { docId } - ); - - if (isResponse(response)) { - const { statusCode } = response; - - if (statusCode !== 200) { - if (statusCode === 500) { - logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); - } else { - logger.debug( - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); - } - } - - response = response.header('accept-ranges', 'none'); - } - - return response; + const downloadResponseHandler = downloadJobResponseHandlerFactory( + config, + elasticsearch, + exportTypesRegistry + ); + + router.get( + { + path: `${MAIN_ENTRY}/download/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + return downloadResponseHandler(res, jobTypes, user, { docId }); + }) + ); // allow a report to be deleted - const getRouteConfigDelete = getRouteConfigFactoryDeletePre(config, plugins, logger); const deleteResponseHandler = deleteJobResponseHandlerFactory(config, elasticsearch); - server.route({ - path: `${MAIN_ENTRY}/delete/{docId}`, - method: 'DELETE', - options: getRouteConfigDelete(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - let response = await deleteResponseHandler( - request.pre.management.jobTypes, - request.pre.user, - h, - { docId } - ); - - if (isResponse(response)) { - const { statusCode } = response; - - if (statusCode !== 200) { - if (statusCode === 500) { - logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); - } else { - logger.debug( - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); - } - } - - response = response.header('accept-ranges', 'none'); - } - - return response; + router.delete( + { + path: `${MAIN_ENTRY}/delete/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + return deleteResponseHandler(res, jobTypes, user, { docId }); + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js deleted file mode 100644 index 2c80965432cd2..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; - -describe('authorized_user_pre_routing', function () { - const createMockConfig = (mockConfig = {}) => { - return { - get: (...keys) => mockConfig[keys.join('.')], - kbnConfig: { get: (...keys) => mockConfig[keys.join('.')] }, - }; - }; - const createMockPlugins = (function () { - const getUserStub = jest.fn(); - - return function ({ - securityEnabled = true, - xpackInfoUndefined = false, - xpackInfoAvailable = true, - getCurrentUser = undefined, - user = undefined, - }) { - getUserStub.mockReset(); - getUserStub.mockResolvedValue(user); - return { - security: securityEnabled - ? { - authc: { getCurrentUser }, - } - : null, - __LEGACY: { - plugins: { - xpack_main: { - info: !xpackInfoUndefined && { - isAvailable: () => xpackInfoAvailable, - feature(featureName) { - if (featureName === 'security') { - return { - isEnabled: () => securityEnabled, - isAvailable: () => xpackInfoAvailable, - }; - } - }, - }, - }, - }, - }, - }; - }; - })(); - - const mockRequestRaw = { - body: {}, - events: {}, - headers: {}, - isSystemRequest: false, - params: {}, - query: {}, - route: { settings: { payload: 'abc' }, options: { authRequired: true, body: {}, tags: [] } }, - withoutSecretHeaders: true, - }; - const getMockRequest = () => ({ - ...mockRequestRaw, - raw: { req: mockRequestRaw }, - }); - - const getMockLogger = () => ({ - warn: jest.fn(), - error: (msg) => { - throw new Error(msg); - }, - }); - - it('should return with boom notFound when xpackInfo is undefined', async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ xpackInfoUndefined: true }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(404); - }); - - it(`should return with boom notFound when xpackInfo isn't available`, async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ xpackInfoAvailable: false }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(404); - }); - - it('should return with null user when security is disabled in Elasticsearch', async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ securityEnabled: false }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toBe(null); - }); - - it('should return with boom unauthenticated when security is enabled but no authenticated user', async function () { - const mockPlugins = createMockPlugins({ - user: null, - config: { 'xpack.reporting.roles.allow': ['.reporting_user'] }, - }); - mockPlugins.security = { authc: { getCurrentUser: () => null } }; - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(401); - }); - - it(`should return with boom forbidden when security is enabled but user doesn't have allowed role`, async function () { - const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); - const mockPlugins = createMockPlugins({ - user: { roles: [] }, - getCurrentUser: () => ({ roles: ['something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(403); - }); - - it('should return with user when security is enabled and user has explicitly allowed role', async function () { - const user = { roles: ['.reporting_user', 'something_else'] }; - const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); - const mockPlugins = createMockPlugins({ - user, - getCurrentUser: () => ({ roles: ['.reporting_user', 'something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toEqual(user); - }); - - it('should return with user when security is enabled and user has superuser role', async function () { - const user = { roles: ['superuser', 'something_else'] }; - const mockConfig = createMockConfig({ 'roles.allow': [] }); - const mockPlugins = createMockPlugins({ - getCurrentUser: () => ({ roles: ['superuser', 'something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toEqual(user); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts new file mode 100644 index 0000000000000..4cb7af3d0d409 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; +import sinon from 'sinon'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { ReportingConfig, ReportingCore } from '../../'; +import { createMockReportingCore } from '../../../test_helpers'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; +import { ReportingInternalSetup } from '../../core'; + +let mockConfig: ReportingConfig; +let mockCore: ReportingCore; + +const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, +}); + +const getMockContext = () => + (({ + core: coreMock.createRequestHandlerContext(), + } as unknown) as RequestHandlerContext); + +const getMockRequest = () => + ({ + url: { port: '5601', query: '', path: '/foo' }, + route: { path: '/foo', options: {} }, + } as KibanaRequest); + +const getMockResponseFactory = () => + (({ + ...httpServerMock.createResponseFactory(), + forbidden: (obj: unknown) => obj, + unauthorized: (obj: unknown) => obj, + } as unknown) as KibanaResponseFactory); + +beforeEach(async () => { + const mockConfigGet = sinon.stub().withArgs('roles', 'allow').returns(['reporting_user']); + mockConfig = getMockConfig(mockConfigGet); + mockCore = await createMockReportingCore(mockConfig); +}); + +describe('authorized_user_pre_routing', function () { + it('should return from handler with null user when security is disabled', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: undefined, // disable security + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; + + let handlerCalled = false; + authorizedUserPreRouting((user: unknown) => { + expect(user).toBe(null); // verify the user is a null value + handlerCalled = true; + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + + expect(handlerCalled).toBe(true); + }); + + it('should return with 401 when security is enabled but no authenticated user', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { getCurrentUser: () => null }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + const requestHandler = authorizedUserPreRouting(mockHandler); + const mockResponseFactory = getMockResponseFactory(); + + expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({ + body: `Sorry, you aren't authenticated`, + }); + }); + + it(`should return with 403 when security is enabled but user doesn't have allowed role`, async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + expect( + authorizedUserPreRouting(mockHandler)(getMockContext(), getMockRequest(), mockResponseFactory) + ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); + }); + + it('should return from handler when security is enabled and user has explicitly allowed role', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => ({ username: 'friendlyuser', roles: ['reporting_user'] }), + }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + let handlerCalled = false; + authorizedUserPreRouting((user: unknown) => { + expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); + handlerCalled = true; + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + + expect(handlerCalled).toBe(true); + }); + + it('should return from handler when security is enabled and user has superuser role', async function () {}); +}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 0c4e75a53831e..87582ca3ca239 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -4,52 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { Legacy } from 'kibana'; +import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/server'; -import { ReportingConfig } from '../../../server'; -import { LevelLogger as Logger } from '../../../server/lib'; -import { ReportingSetupDeps } from '../../../server/types'; import { getUserFactory } from '../../lib/get_user'; +import { ReportingCore } from '../../core'; +type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; -export type PreRoutingFunction = ( - request: Legacy.Request -) => Promise | AuthenticatedUser | null>; +export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R + ? (user: ReportingUser, ...a: U) => R + : never; export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger + reporting: ReportingCore ) { - const getUser = getUserFactory(plugins.security, logger); - const { info: xpackInfo } = plugins.__LEGACY.plugins.xpack_main; - - return async function authorizedUserPreRouting(request: Legacy.Request) { - if (!xpackInfo || !xpackInfo.isAvailable()) { - logger.warn('Unable to authorize user before xpack info is available.', [ - 'authorizedUserPreRouting', - ]); - return Boom.notFound(); - } - - const security = xpackInfo.feature('security'); - if (!security.isEnabled() || !security.isAvailable()) { - return null; - } - - const user = await getUser(request); - - if (!user) { - return Boom.unauthorized(`Sorry, you aren't authenticated`); - } - - const authorizedRoles = [superuserRole, ...(config.get('roles', 'allow') as string[])]; - if (!user.roles.find((role) => authorizedRoles.includes(role))) { - return Boom.forbidden(`Sorry, you don't have access to Reporting`); - } - - return user; + const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); + const getUser = getUserFactory(setupDeps.security); + return (handler: RequestHandlerUser): RequestHandler => { + return (context, req, res) => { + let user: ReportingUser = null; + if (setupDeps.security) { + // find the authenticated user, or null if security is not enabled + user = getUser(req); + if (!user) { + // security is enabled but the user is null + return res.unauthorized({ body: `Sorry, you aren't authenticated` }); + } + } + + if (user) { + // check allowance with the configured set of roleas + "superuser" + const allowedRoles = config.get('roles', 'allow') || []; + const authorizedRoles = [superuserRole, ...allowedRoles]; + + if (!user.roles.find((role) => authorizedRoles.includes(role))) { + // user's roles do not allow + return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); + } + } + + return handler(user, context, req, res); + }; }; }; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index 6a228c1915615..e16f5278c8cc7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -12,15 +12,10 @@ import { statuses } from '../../lib/esqueue/constants/statuses'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; import { ExportTypeDefinition, JobDocOutput, JobSource } from '../../types'; -interface ICustomHeaders { - [x: string]: any; -} - type ExportTypeType = ExportTypeDefinition; interface ErrorFromPayload { message: string; - reason: string | null; } // A camelCase version of JobDocOutput @@ -37,7 +32,7 @@ const getTitle = (exportType: ExportTypeType, title?: string): string => `${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`; const getReportingHeaders = (output: JobDocOutput, exportType: ExportTypeType) => { - const metaDataHeaders: ICustomHeaders = {}; + const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE) { const csvContainsFormulas = _.get(output, 'csv_contains_formulas', false); @@ -76,12 +71,13 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist }; } + // @TODO: These should be semantic HTTP codes as 500/503's indicate + // error then these are really operating properly. function getFailure(output: JobDocOutput): Payload { return { statusCode: 500, content: { - message: 'Reporting generation failed', - reason: output.content, + message: `Reporting generation failed: ${output.content}`, }, contentType: 'application/json', headers: {}, @@ -92,7 +88,7 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist return { statusCode: 503, content: status, - contentType: 'application/json', + contentType: 'text/plain', headers: { 'retry-after': 30 }, }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 174ec15c81d8a..990af2d0aca80 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { ResponseToolkit } from 'hapi'; -import { ElasticsearchServiceSetup } from 'kibana/server'; +import { ElasticsearchServiceSetup, kibanaResponseFactory } from 'kibana/server'; +import { AuthenticatedUser } from '../../../../../../plugins/security/server'; import { ReportingConfig } from '../../'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; @@ -29,40 +28,43 @@ export function downloadJobResponseHandlerFactory( const jobsQuery = jobsQueryFactory(config, elasticsearch); const getDocumentPayload = getDocumentPayloadFactory(exportTypesRegistry); - return function jobResponseHandler( + return async function jobResponseHandler( + res: typeof kibanaResponseFactory, validJobTypes: string[], - user: any, - h: ResponseToolkit, + user: AuthenticatedUser | null, params: JobResponseHandlerParams, opts: JobResponseHandlerOpts = {} ) { const { docId } = params; - // TODO: async/await - return jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }).then((doc) => { - if (!doc) return Boom.notFound(); - const { jobtype: jobType } = doc._source; - if (!validJobTypes.includes(jobType)) { - return Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } + const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }); + if (!doc) { + return res.notFound(); + } - const output = getDocumentPayload(doc); + const { jobtype: jobType } = doc._source; - if (!WHITELISTED_JOB_CONTENT_TYPES.includes(output.contentType)) { - return Boom.badImplementation( - `Unsupported content-type of ${output.contentType} specified by job output` - ); - } + if (!validJobTypes.includes(jobType)) { + return res.unauthorized({ + body: `Sorry, you are not authorized to download ${jobType} reports`, + }); + } - const response = h.response(output.content).type(output.contentType).code(output.statusCode); + const response = getDocumentPayload(doc); - if (output.headers) { - Object.keys(output.headers).forEach((key) => { - response.header(key, output.headers[key]); - }); - } + if (!WHITELISTED_JOB_CONTENT_TYPES.includes(response.contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${response.contentType} specified by job output`, + }); + } - return response; // Hapi + return res.custom({ + body: typeof response.content === 'string' ? Buffer.from(response.content) : response.content, + statusCode: response.statusCode, + headers: { + ...response.headers, + 'content-type': response.contentType, + }, }); }; } @@ -74,26 +76,37 @@ export function deleteJobResponseHandlerFactory( const jobsQuery = jobsQueryFactory(config, elasticsearch); return async function deleteJobResponseHander( + res: typeof kibanaResponseFactory, validJobTypes: string[], - user: any, - h: ResponseToolkit, + user: AuthenticatedUser | null, params: JobResponseHandlerParams ) { const { docId } = params; const doc = await jobsQuery.get(user, docId, { includeContent: false }); - if (!doc) return Boom.notFound(); + + if (!doc) { + return res.notFound(); + } const { jobtype: jobType } = doc._source; + if (!validJobTypes.includes(jobType)) { - return Boom.unauthorized(`Sorry, you are not authorized to delete ${jobType} reports`); + return res.unauthorized({ + body: `Sorry, you are not authorized to delete ${jobType} reports`, + }); } try { const docIndex = doc._index; await jobsQuery.delete(docIndex, docId); - return h.response({ deleted: true }); + return res.ok({ + body: { deleted: true }, + }); } catch (error) { - return Boom.boomify(error, { statusCode: error.statusCode }); + return res.customError({ + statusCode: error.statusCode, + body: error.message, + }); } }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts deleted file mode 100644 index 8cdb7b4c018d7..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -import { makeRequestFacade } from './make_request_facade'; - -describe('makeRequestFacade', () => { - test('creates a default object', () => { - const legacyRequest = ({ - getBasePath: () => 'basebase', - params: { - param1: 123, - }, - payload: { - payload1: 123, - }, - headers: { - user: 123, - }, - } as unknown) as Legacy.Request; - - expect(makeRequestFacade(legacyRequest)).toMatchInlineSnapshot(` - Object { - "getBasePath": [Function], - "getRawRequest": [Function], - "getSavedObjectsClient": undefined, - "headers": Object { - "user": 123, - }, - "params": Object { - "param1": 123, - }, - "payload": Object { - "payload1": 123, - }, - "pre": undefined, - "query": undefined, - "route": undefined, - } - `); - }); - - test('getRawRequest', () => { - const legacyRequest = ({ - getBasePath: () => 'basebase', - params: { - param1: 123, - }, - payload: { - payload1: 123, - }, - headers: { - user: 123, - }, - } as unknown) as Legacy.Request; - - expect(makeRequestFacade(legacyRequest).getRawRequest()).toBe(legacyRequest); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts deleted file mode 100644 index 5dd62711f2565..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { RequestQuery } from 'hapi'; -import { Legacy } from 'kibana'; -import { - RequestFacade, - ReportingRequestPayload, - ReportingRequestPre, - ReportingRequestQuery, -} from '../../../server/types'; - -export function makeRequestFacade(request: Legacy.Request): RequestFacade { - // This condition is for unit tests - const getSavedObjectsClient = request.getSavedObjectsClient - ? request.getSavedObjectsClient.bind(request) - : request.getSavedObjectsClient; - return { - getSavedObjectsClient, - headers: request.headers, - params: request.params, - payload: (request.payload as object) as ReportingRequestPayload, - query: ((request.query as RequestQuery) as object) as ReportingRequestQuery, - pre: (request.pre as Record) as ReportingRequestPre, - getBasePath: request.getBasePath, - route: request.route, - getRawRequest: () => request, - }; -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts deleted file mode 100644 index f9c7571e25bac..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { Legacy } from 'kibana'; -import { ReportingConfig } from '../../'; -import { LevelLogger as Logger } from '../../lib'; -import { ReportingSetupDeps } from '../../types'; - -export type GetReportingFeatureIdFn = (request: Legacy.Request) => string; - -export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -) { - const xpackMainPlugin = plugins.__LEGACY.plugins.xpack_main; - const pluginId = 'reporting'; - - // License checking and enable/disable logic - return function reportingFeaturePreRouting(getReportingFeatureId: GetReportingFeatureIdFn) { - return function licensePreRouting(request: Legacy.Request) { - const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); - const reportingFeatureId = getReportingFeatureId(request) as string; - const reportingFeature = licenseCheckResults[reportingFeatureId]; - if (!reportingFeature.showLinks || !reportingFeature.enableLinks) { - throw Boom.forbidden(reportingFeature.message); - } else { - return reportingFeature; - } - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts deleted file mode 100644 index 0ee9db4678684..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { ReportingConfig } from '../../'; -import { LevelLogger as Logger } from '../../lib'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { ReportingSetupDeps } from '../../types'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; -import { - GetReportingFeatureIdFn, - reportingFeaturePreRoutingFactory, -} from './reporting_feature_pre_routing'; - -const API_TAG = 'api'; - -export interface RouteConfigFactory { - tags?: string[]; - pre: any[]; - response?: { - ranges: boolean; - }; -} - -export type GetRouteConfigFactoryFn = ( - getFeatureId?: GetReportingFeatureIdFn -) => RouteConfigFactory; - -export function getRouteConfigFactoryReportingPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); - - return (getFeatureId?: GetReportingFeatureIdFn): RouteConfigFactory => { - const preRouting: any[] = [{ method: authorizedUserPreRouting, assign: 'user' }]; - if (getFeatureId) { - preRouting.push(reportingFeaturePreRouting(getFeatureId)); - } - - return { - tags: [API_TAG], - pre: preRouting, - }; - }; -} - -export function getRouteOptionsCsv( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -) { - const getRouteConfig = getRouteConfigFactoryReportingPre(config, plugins, logger); - return { - ...getRouteConfig(() => CSV_FROM_SAVEDOBJECT_JOB_TYPE), - validate: { - params: Joi.object({ - savedObjectType: Joi.string().required(), - savedObjectId: Joi.string().required(), - }).required(), - payload: Joi.object({ - state: Joi.object().default({}), - timerange: Joi.object({ - timezone: Joi.string().default('UTC'), - min: Joi.date().required(), - max: Joi.date().required(), - }).optional(), - }), - }, - }; -} - -export function getRouteConfigFactoryManagementPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); - const managementPreRouting = reportingFeaturePreRouting(() => 'management'); - - return (): RouteConfigFactory => { - return { - pre: [ - { method: authorizedUserPreRouting, assign: 'user' }, - { method: managementPreRouting, assign: 'management' }, - ], - }; - }; -} - -// NOTE: We're disabling range request for downloading the PDF. There's a bug in Firefox's PDF.js viewer -// (https://github.com/mozilla/pdf.js/issues/8958) where they're using a range request to retrieve the -// TOC at the end of the PDF, but it's sending multiple cookies and causing our auth to fail with a 401. -// Additionally, the range-request doesn't alleviate any performance issues on the server as the entire -// download is loaded into memory. -export function getRouteConfigFactoryDownloadPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); - return (): RouteConfigFactory => ({ - ...getManagementRouteConfig(), - tags: [API_TAG, 'download'], - response: { - ranges: false, - }, - }); -} - -export function getRouteConfigFactoryDeletePre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); - return (): RouteConfigFactory => ({ - ...getManagementRouteConfig(), - tags: [API_TAG, 'delete'], - response: { - ranges: false, - }, - }); -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts index 2ebe1ada418dc..afa3fd3358fc1 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { KibanaResponseFactory, KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../../../plugins/security/common/model/authenticated_user'; import { JobDocPayload } from '../types'; export type HandlerFunction = ( + user: AuthenticatedUser | null, exportType: string, jobParams: object, - request: Legacy.Request, - h: ReportingResponseToolkit + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory ) => any; -export type HandlerErrorFunction = (exportType: string, err: Error) => any; +export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; export interface QueuedJobPayload { error?: boolean; @@ -24,5 +27,3 @@ export interface QueuedJobPayload { }; }; } - -export type ReportingResponseToolkit = Legacy.ResponseToolkit; diff --git a/x-pack/legacy/plugins/reporting/server/types.ts b/x-pack/legacy/plugins/reporting/server/types.ts index bfab568fe9fb3..2ccc209c3ce50 100644 --- a/x-pack/legacy/plugins/reporting/server/types.ts +++ b/x-pack/legacy/plugins/reporting/server/types.ts @@ -5,6 +5,7 @@ */ import { Legacy } from 'kibana'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -53,8 +54,8 @@ export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPa export interface TimeRangeParams { timezone: string; - min: Date | string | number; - max: Date | string | number; + min: Date | string | number | null; + max: Date | string | number | null; } export interface JobParamPostPayload { @@ -189,22 +190,10 @@ export interface LegacySetup { * Internal Types */ -export interface RequestFacade { - getBasePath: Legacy.Request['getBasePath']; - getSavedObjectsClient: Legacy.Request['getSavedObjectsClient']; - headers: Legacy.Request['headers']; - params: Legacy.Request['params']; - payload: JobParamPostPayload | GenerateExportTypePayload; - query: ReportingRequestQuery; - route: Legacy.Request['route']; - pre: ReportingRequestPre; - getRawRequest: () => Legacy.Request; -} - export type ESQueueCreateJobFn = ( jobParams: JobParamsType, - headers: Record, - request: RequestFacade + context: RequestHandlerContext, + request: KibanaRequest ) => Promise; export type ESQueueWorkerExecuteFn = ( diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts index 286e072f1f8f6..f6dbccdfe3980 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts @@ -12,17 +12,21 @@ jest.mock('../server/lib/create_queue'); jest.mock('../server/lib/enqueue_job'); jest.mock('../server/lib/validate'); +import { of } from 'rxjs'; import { EventEmitter } from 'events'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { coreMock } from 'src/core/server/mocks'; import { ReportingConfig, ReportingCore, ReportingPlugin } from '../server'; import { ReportingSetupDeps, ReportingStartDeps } from '../server/types'; +import { ReportingInternalSetup } from '../server/core'; const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => { return { elasticsearch: setupMock.elasticsearch, security: setupMock.security, - licensing: {} as any, + licensing: { + license$: of({ isAvailable: true, isActive: true, type: 'basic' }), + } as any, usageCollection: {} as any, __LEGACY: { plugins: { xpack_main: { status: new EventEmitter() } } } as any, }; @@ -49,8 +53,18 @@ const createMockReportingPlugin = async (config: ReportingConfig): Promise => { +export const createMockReportingCore = async ( + config: ReportingConfig, + setupDepsMock?: ReportingInternalSetup +): Promise => { config = config || {}; const plugin = await createMockReportingPlugin(config); - return plugin.getReportingCore(); + const core = plugin.getReportingCore(); + + if (setupDepsMock) { + // @ts-ignore overwriting private properties + core.pluginSetupDeps = setupDepsMock; + } + + return core; }; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts index 819636b714631..01b9f6cbd9cd6 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts @@ -4,9 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServerFacade } from '../server/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createHttpServer, createCoreContext } from 'src/core/server/http/test_utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ContextService } from 'src/core/server/context/context_service'; -export const createMockServer = (): ServerFacade => { - const mockServer = {}; - return mockServer as any; +const coreId = Symbol('reporting'); + +export const createMockServer = async () => { + const coreContext = createCoreContext({ coreId }); + const contextService = new ContextService(coreContext); + + const server = createHttpServer(coreContext); + const httpSetup = await server.setup({ + context: contextService.setup({ pluginDependencies: new Map() }), + }); + const handlerContext = coreMock.createRequestHandlerContext(); + + httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => { + return handlerContext; + }); + + return { + server, + httpSetup, + handlerContext, + }; }; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index d068711b87c9d..e44bd92c42391 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -13,7 +13,8 @@ "uiActions", "embeddable", "share", - "kibanaLegacy" + "kibanaLegacy", + "licensing" ], "server": true, "ui": true diff --git a/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts b/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts index 7d11403add136..90f97d44da224 100644 --- a/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts +++ b/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts @@ -46,7 +46,7 @@ export default function ({ getService }: FtrProviderContext) { jobParams: 0, })) as supertest.Response; - expect(resText).to.match(/\\\"jobParams\\\" must be a string/); + expect(resText).to.match(/expected value of type \[string\] but got \[number\]/); expect(resStatus).to.eql(400); }); From d0aeadf13eb8f7621d21f41eae480f011a561e64 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 30 May 2020 01:01:59 +0100 Subject: [PATCH 16/69] chore(NA): use env var to point config folder on os_packages built with fpm (#67433) Co-authored-by: Elastic Machine --- src/core/server/path/index.ts | 1 - .../tasks/os_packages/service_templates/sysv/etc/default/kibana | 2 ++ .../tasks/os_packages/service_templates/sysv/etc/init.d/kibana | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/server/path/index.ts b/src/core/server/path/index.ts index d482a32b32ae4..2e05e3856bd4c 100644 --- a/src/core/server/path/index.ts +++ b/src/core/server/path/index.ts @@ -28,7 +28,6 @@ const CONFIG_PATHS = [ process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), process.env.CONFIG_PATH, // deprecated fromRoot('config/kibana.yml'), - '/etc/kibana/kibana.yml', ].filter(isString); const DATA_PATHS = [ diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana index 7b411542986be..092dc6482fa1d 100644 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana @@ -11,3 +11,5 @@ nice="" KILL_ON_STOP_TIMEOUT=0 BABEL_CACHE_PATH="/var/lib/kibana/optimize/.babel_register_cache.json" + +KIBANA_PATH_CONF="/etc/kibana" diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index fc1b797fe8ed2..a17d15522b45e 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -23,6 +23,7 @@ pidfile="/var/run/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name +export KIBANA_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 From 96ef01828ce0a0083dad4ad4aa104feea9e6d60c Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Sat, 30 May 2020 09:51:07 +0200 Subject: [PATCH 17/69] [SIEM] Covers 'Import query from saved timeline' functionality with Cypress (#67459) * modifies 'Creates and activates a new custom rule' test to cover 'import query from saved timeline' functionality * adds missing files Co-authored-by: Elastic Machine --- .../signal_detection_rules_custom.spec.ts | 8 +- x-pack/plugins/siem/cypress/objects/rule.ts | 4 +- .../siem/cypress/screens/create_new_rule.ts | 3 + .../plugins/siem/cypress/screens/timeline.ts | 4 + .../siem/cypress/tasks/create_new_rule.ts | 11 + .../rules/step_define_rule/index.tsx | 5 +- .../custom_rule_with_timeline/data.json.gz | Bin 0 -> 67934 bytes .../custom_rule_with_timeline/mappings.json | 7983 +++++++++++++++++ 8 files changed, 8012 insertions(+), 6 deletions(-) create mode 100644 x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz create mode 100644 x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts index 4b5e12124dd40..04762bbf352d2 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts @@ -41,7 +41,7 @@ import { import { createAndActivateRule, fillAboutRuleAndContinue, - fillDefineCustomRuleAndContinue, + fillDefineCustomRuleWithImportedQueryAndContinue, } from '../tasks/create_new_rule'; import { goToManageSignalDetectionRules, @@ -66,11 +66,11 @@ import { DETECTIONS } from '../urls/navigation'; describe('Signal detection rules, custom', () => { before(() => { - esArchiverLoad('prebuilt_rules_loaded'); + esArchiverLoad('custom_rule_with_timeline'); }); after(() => { - esArchiverUnload('prebuilt_rules_loaded'); + esArchiverUnload('custom_rule_with_timeline'); }); it('Creates and activates a new custom rule', () => { @@ -80,7 +80,7 @@ describe('Signal detection rules, custom', () => { goToManageSignalDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); goToCreateNewRule(); - fillDefineCustomRuleAndContinue(newRule); + fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); createAndActivateRule(); diff --git a/x-pack/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/siem/cypress/objects/rule.ts index 7ce8aa69f3339..d750fe212002d 100644 --- a/x-pack/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/plugins/siem/cypress/objects/rule.ts @@ -28,6 +28,7 @@ export interface CustomRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; + timelineId: string; } export interface MachineLearningRule { @@ -56,7 +57,7 @@ const mitre2: Mitre = { }; export const newRule: CustomRule = { - customQuery: 'hosts.name: *', + customQuery: 'host.name: *', name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -66,6 +67,7 @@ export const newRule: CustomRule = { falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], note: '# test markdown', + timelineId: '352c6110-9ffb-11ea-b3d8-857d6042d9bd', }; export const machineLearningRule: MachineLearningRule = { diff --git a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts index db9866cdf7f63..bc0740554bc52 100644 --- a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts @@ -24,6 +24,9 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; +export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = + '[data-test-subj="importQueryFromSavedTimeline"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index 58d2568084f7c..ed1dc97454fb3 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -21,6 +21,10 @@ export const SEARCH_OR_FILTER_CONTAINER = export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; +export const TIMELINE = (id: string) => { + return `[data-test-subj="title-${id}"]`; +}; + export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS_EMPTY = diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts index 6324b42f3783a..eca5885e7b3d9 100644 --- a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts @@ -14,6 +14,7 @@ import { CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, + IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +31,7 @@ import { SEVERITY_DROPDOWN, TAGS_INPUT, } from '../screens/create_new_rule'; +import { TIMELINE } from '../screens/timeline'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); @@ -86,6 +88,15 @@ export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); + cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery); + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + + cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); +}; + export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRule) => { cy.get(MACHINE_LEARNING_DROPDOWN).click({ force: true }); cy.contains(MACHINE_LEARNING_LIST, rule.machineLearningJob).click(); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx index 119f851ecdfe4..fc875908bd4ef 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx @@ -203,7 +203,10 @@ const StepDefineRuleComponent: FC = ({ config={{ ...schema.queryBar, labelAppend: ( - + {i18n.IMPORT_TIMELINE_QUERY} ), diff --git a/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..3d50451cee39fe8c8f8b49da585473585a812c1d GIT binary patch literal 67934 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 literal 0 HcmV?d00001 diff --git a/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json new file mode 100644 index 0000000000000..d01e6344bcfaf --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json @@ -0,0 +1,7983 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "epm-packages": "92b4b1899b887b090d01c033f3118a85", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "864760267df6c970f629bd4458506c53", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "ingest-agent-configs": "d9a5cbdce8e937f674a7b376c47a34a1", + "ingest-datasources": "c0fe6347b0eebcbf421841669e3acd31", + "ingest-outputs": "0e57221778a7153c8292edf154099036", + "ingest_manager_settings": "c5b0749b4ab03c582efd4c14cb8f132c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "17ec409954864e592ceec0c5eae29ad9", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "dynamic": "false", + "type": "object" + }, + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "agent_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoPointFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoShapeFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-agent-configs": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-datasources": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "map": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "endpoint": { + "properties": { + "artifact": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "process": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "entry_modified": { + "type": "double" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "macro": { + "properties": { + "code_page": { + "type": "long" + }, + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "file_extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "stream": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code_size": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "quarantine_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "quarantine_result": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "temp_file_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "file": { + "properties": { + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 6a8b07fe8ea1d33d9377e289df250439a38de1a5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Sat, 30 May 2020 21:22:42 +0200 Subject: [PATCH 18/69] Fix visualize and lens telemetry (#67749) --- x-pack/plugins/lens/server/usage/task.ts | 3 ++- .../server/lib/tasks/visualizations/task_runner.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 5a5d26fa2afde..cde6e7eb6c090 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -6,6 +6,7 @@ import { APICaller, CoreSetup, Logger } from 'kibana/server'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import moment from 'moment'; import { RunContext, @@ -191,7 +192,7 @@ export function telemetryTaskRunner( return { async run() { - const kibanaIndex = (await config.toPromise()).kibana.index; + const kibanaIndex = (await config.pipe(first()).toPromise()).kibana.index; return Promise.all([ getDailyEvents(kibanaIndex, callCluster), diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index 346cc75bb9b24..b15ead36a75f6 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -6,6 +6,8 @@ import { Observable } from 'rxjs'; import _, { countBy, groupBy, mapValues } from 'lodash'; +import { first } from 'rxjs/operators'; + import { APICaller, IClusterClient } from 'src/core/server'; import { getNextMidnight } from '../../get_next_midnight'; import { TaskInstance } from '../../../../../task_manager/server'; @@ -80,7 +82,7 @@ export function visualizationsTaskRunner( let error; try { - const index = (await config.toPromise()).kibana.index; + const index = (await config.pipe(first()).toPromise()).kibana.index; stats = await getStats((await esClientPromise).callAsInternalUser, index); } catch (err) { if (err.constructor === Error) { From 96e0e911ea3a63e1d174d2f1583da59e609b3088 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Sat, 30 May 2020 18:52:01 -0600 Subject: [PATCH 19/69] [SIEM][Lists] Adds test mocks and README.md to the lists plugin ## Summary * https://github.com/elastic/kibana/issues/67675 * Adds README.md to the lists plugin * Adds the mocks to the server side of the lists plugin * Changes out the SIEM code to use the mocks now that they are within the plugin ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [x] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- x-pack/plugins/lists/README.md | 254 ++++++++++++++++++ .../exception_list_item_schema.mock.ts | 40 +++ .../response/exception_list_schema.mock.ts | 24 ++ .../found_exception_list_item_schema.mock.ts | 15 ++ .../found_exception_list_schema.mock.ts | 15 ++ .../response/found_list_item_schema.mock.ts | 16 ++ .../response/found_list_schema.mock.ts | 16 ++ .../lists/public/exceptions/__mocks__/api.ts | 15 +- .../lists/public/exceptions/api.test.ts | 33 ++- .../hooks/persist_exception_item.test.tsx | 6 +- .../hooks/persist_exception_list.test.tsx | 6 +- .../plugins/lists/public/exceptions/mock.ts | 52 ---- x-pack/plugins/lists/public/index.tsx | 7 +- x-pack/plugins/lists/server/get_user.test.ts | 13 +- x-pack/plugins/lists/server/mocks.ts | 23 ++ .../exception_list_client.mock.ts | 35 +++ .../exception_list_client.test.ts | 34 +++ .../server/services/lists/list_client.mock.ts | 69 +++++ .../server/services/lists/list_client.test.ts | 30 +++ .../lists/server/services/lists/types.ts | 2 +- .../services/utils/get_search_after_scroll.ts | 1 + .../services/utils/scroll_to_start_page.ts | 1 + .../plugins/siem/public/lists_plugin_deps.ts | 2 - .../signals/filter_events_with_list.test.ts | 98 +++---- .../signals/search_after_bulk_create.test.ts | 82 +++--- .../signals/search_after_bulk_create.ts | 2 +- .../signals/signal_rule_alert_type.test.ts | 9 +- 27 files changed, 690 insertions(+), 210 deletions(-) create mode 100644 x-pack/plugins/lists/README.md create mode 100644 x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/server/mocks.ts create mode 100644 x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts create mode 100644 x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts create mode 100644 x-pack/plugins/lists/server/services/lists/list_client.mock.ts create mode 100644 x-pack/plugins/lists/server/services/lists/list_client.test.ts diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md new file mode 100644 index 0000000000000..cb343c95b0103 --- /dev/null +++ b/x-pack/plugins/lists/README.md @@ -0,0 +1,254 @@ +README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + +The scripts rely on CURL and jq: + +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) + +Install curl and jq (mac instructions) + +```sh +brew update +brew install curl +brew install jq +``` + +Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL` +and add these environment variables: + +```sh +export ELASTICSEARCH_USERNAME=${user} +export ELASTICSEARCH_PASSWORD=${password} +export ELASTICSEARCH_URL=https://${ip}:9200 +export KIBANA_URL=http://localhost:5601 +export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} +export KIBANA_INDEX=.kibana-${your user id} +``` + +source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: + +```sh +source ~/.zshrc +``` + +Open your `kibana.dev.yml` file and add these lines: + +```sh +# Enable lists feature +xpack.lists.enabled: true +xpack.lists.listIndex: '.lists-frank' +xpack.lists.listItemIndex: '.items-frank' +``` + +Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will +get in the way of the CURL scripts written as is. + +Go to the scripts folder `cd kibana/x-pack/plugins/lists/server/scripts` and run: + +```sh +./hard_reset.sh +./post_list.sh +``` + +which will: + +- Delete any existing lists you have +- Delete any existing list items you have +- Delete any existing exception lists you have +- Delete any existing exception list items you have +- Delete any existing mapping, policies, and templates, you might have previously had. +- Add the latest list and list item index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.lists.listIndex` and `xpack.lists.listItemIndex`. +- Posts the sample list from `./lists/new/list_ip.json` + +Now you can run + +```sh +./post_list.sh +``` + +You should see the new list created like so: + +```sh +{ + "id": "list-ip", + "created_at": "2020-05-28T19:15:22.344Z", + "created_by": "yo", + "description": "This list describes bad internet ip", + "name": "Simple list with an ip", + "tie_breaker_id": "c57efbc4-4977-4a32-995f-cfd296bed521", + "type": "ip", + "updated_at": "2020-05-28T19:15:22.344Z", + "updated_by": "yo" +} +``` + +You can add a list item like so: + +```sh + ./post_list_item.sh +``` + +You should see the new list item created and attached to the above list like so: + +```sh +{ + "id": "hand_inserted_item_id", + "type": "ip", + "value": "127.0.0.1", + "created_at": "2020-05-28T19:15:49.790Z", + "created_by": "yo", + "list_id": "list-ip", + "tie_breaker_id": "a881bf2e-1e17-4592-bba8-d567cb07d234", + "updated_at": "2020-05-28T19:15:49.790Z", + "updated_by": "yo" +} +``` + +If you want to post an exception list it would be like so: + +```sh +./post_exception_list.sh +``` + +You should see the new exception list created like so: + +```sh +{ + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-05-28T19:16:31.052Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "id": "bcb94680-a117-11ea-ad9d-c71f4820e65b", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "86e08c8c-c970-4b08-a6e2-cdba7bb4e023", + "type": "endpoint", + "updated_at": "2020-05-28T19:16:31.080Z", + "updated_by": "yo" +} +``` + +And you can attach exception list items like so: + +```ts +{ + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "comment": [], + "created_at": "2020-05-28T19:17:21.099Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "match": "Elastic, N.V." + }, + { + "field": "event.category", + "operator": "included", + "match_any": [ + "process", + "malware" + ] + } + ], + "id": "da8d3b30-a117-11ea-ad9d-c71f4820e65b", + "item_id": "endpoint_list_item", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "21f84703-9476-4af8-a212-aad31e18dcb9", + "type": "simple", + "updated_at": "2020-05-28T19:17:21.123Z", + "updated_by": "yo" +} +``` + +You can then do find for each one like so: + +```sh +./find_lists.sh +``` + +```sh +{ + "cursor": "WzIwLFsiYzU3ZWZiYzQtNDk3Ny00YTMyLTk5NWYtY2ZkMjk2YmVkNTIxIl1d", + "data": [ + { + "id": "list-ip", + "created_at": "2020-05-28T19:15:22.344Z", + "created_by": "yo", + "description": "This list describes bad internet ip", + "name": "Simple list with an ip", + "tie_breaker_id": "c57efbc4-4977-4a32-995f-cfd296bed521", + "type": "ip", + "updated_at": "2020-05-28T19:15:22.344Z", + "updated_by": "yo" + } + ], + "page": 1, + "per_page": 20, + "total": 1 +} +``` + +or for finding exception lists: + +```sh +./find_exception_lists.sh +``` + +```sh +{ + "data": [ + { + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-05-28T19:16:31.052Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "id": "bcb94680-a117-11ea-ad9d-c71f4820e65b", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "86e08c8c-c970-4b08-a6e2-cdba7bb4e023", + "type": "endpoint", + "updated_at": "2020-05-28T19:16:31.080Z", + "updated_by": "yo" + } + ], + "page": 1, + "per_page": 20, + "total": 1 +} +``` + +See the full scripts folder for all the capabilities. diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..901715b601b80 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionListItemSchema } from './exception_list_item_schema'; + +export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ + _tags: ['endpoint', 'process', 'malware', 'os:linux'], + comment: [], + created_at: '2020-04-23T00:19:13.289Z', + created_by: 'user_name', + description: 'This is a sample endpoint type exception', + entries: [ + { + field: 'actingProcess.file.signer', + match: 'Elastic, N.V.', + match_any: undefined, + operator: 'included', + }, + { + field: 'event.category', + match: undefined, + match_any: ['process', 'malware'], + operator: 'included', + }, + ], + id: '1', + item_id: 'endpoint_list_item', + list_id: 'endpoint_list', + meta: {}, + name: 'Sample Endpoint Exception List', + namespace_type: 'single', + tags: ['user added string for a tag', 'malware'], + tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', + type: 'simple', + updated_at: '2020-04-23T00:19:13.289Z', + updated_by: 'user_name', +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts new file mode 100644 index 0000000000000..017b959a2baf3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionListSchema } from './exception_list_schema'; + +export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ + _tags: ['endpoint', 'process', 'malware', 'os:linux'], + created_at: '2020-04-23T00:19:13.289Z', + created_by: 'user_name', + description: 'This is a sample endpoint type exception', + id: '1', + list_id: 'endpoint_list', + meta: {}, + name: 'Sample Endpoint Exception List', + namespace_type: 'single', + tags: ['user added string for a tag', 'malware'], + tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', + type: 'endpoint', + updated_at: '2020-04-23T00:19:13.289Z', + updated_by: 'user_name', +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f760e602605ba --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; +import { FoundExceptionListItemSchema } from './found_exception_list_item_schema'; + +export const getFoundExceptionListItemSchemaMock = (): FoundExceptionListItemSchema => ({ + data: [getExceptionListItemSchemaMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts new file mode 100644 index 0000000000000..ce71a27dbc4d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListSchemaMock } from './exception_list_schema.mock'; +import { FoundExceptionListSchema } from './found_exception_list_schema'; + +export const getFoundExceptionListSchemaMock = (): FoundExceptionListSchema => ({ + data: [getExceptionListSchemaMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts new file mode 100644 index 0000000000000..e96188c619d78 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FoundListItemSchema } from './found_list_item_schema'; +import { getListItemResponseMock } from './list_item_schema.mock'; + +export const getFoundListItemSchemaMock = (): FoundListItemSchema => ({ + cursor: '123', + data: [getListItemResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts new file mode 100644 index 0000000000000..63d6a3b220ac1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FoundListSchema } from './found_list_schema'; +import { getListResponseMock } from './list_schema.mock'; + +export const getFoundListSchemaMock = (): FoundListSchema => ({ + cursor: '123', + data: [getListResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts index f624189915dcf..787d374ab2cad 100644 --- a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts +++ b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -15,37 +17,38 @@ import { ApiCallByIdProps, ApiCallByListIdProps, } from '../types'; -import { mockExceptionItem, mockExceptionList } from '../mock'; /* eslint-disable @typescript-eslint/no-unused-vars */ export const addExceptionList = async ({ http, list, signal, -}: AddExceptionListProps): Promise => Promise.resolve(mockExceptionList); +}: AddExceptionListProps): Promise => + Promise.resolve(getExceptionListSchemaMock()); export const addExceptionListItem = async ({ http, listItem, signal, }: AddExceptionListItemProps): Promise => - Promise.resolve(mockExceptionItem); + Promise.resolve(getExceptionListItemSchemaMock()); export const fetchExceptionListById = async ({ http, id, signal, -}: ApiCallByIdProps): Promise => Promise.resolve(mockExceptionList); +}: ApiCallByIdProps): Promise => Promise.resolve(getExceptionListSchemaMock()); export const fetchExceptionListItemsByListId = async ({ http, listId, signal, }: ApiCallByListIdProps): Promise => - Promise.resolve({ data: [mockExceptionItem], page: 1, per_page: 20, total: 1 }); + Promise.resolve({ data: [getExceptionListItemSchemaMock()], page: 1, per_page: 20, total: 1 }); export const fetchExceptionListItemById = async ({ http, id, signal, -}: ApiCallByIdProps): Promise => Promise.resolve(mockExceptionItem); +}: ApiCallByIdProps): Promise => + Promise.resolve(getExceptionListItemSchemaMock()); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 3a61140e5621d..18a89071e9887 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { createKibanaCoreStartMock } from '../common/mocks/kibana_core'; +import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; -import { - mockExceptionItem, - mockExceptionList, - mockNewExceptionItem, - mockNewExceptionList, -} from './mock'; +import { mockNewExceptionItem, mockNewExceptionList } from './mock'; import { addExceptionList, addExceptionListItem, @@ -43,7 +40,7 @@ describe('Exceptions Lists API', () => { describe('addExceptionList', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body', async () => { @@ -63,7 +60,7 @@ describe('Exceptions Lists API', () => { test('check parameter url, body when "list.id" exists', async () => { await addExceptionList({ http: mockKibanaHttpService(), - list: mockExceptionList, + list: getExceptionListSchemaMock(), signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { @@ -80,14 +77,14 @@ describe('Exceptions Lists API', () => { list: mockNewExceptionList, signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); describe('addExceptionListItem', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionItem); + fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body', async () => { @@ -107,7 +104,7 @@ describe('Exceptions Lists API', () => { test('check parameter url, body when "listItem.id" exists', async () => { await addExceptionListItem({ http: mockKibanaHttpService(), - listItem: mockExceptionItem, + listItem: getExceptionListItemSchemaMock(), signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { @@ -124,14 +121,14 @@ describe('Exceptions Lists API', () => { listItem: mockNewExceptionItem, signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionItem); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); describe('fetchExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body', async () => { @@ -155,7 +152,7 @@ describe('Exceptions Lists API', () => { id: '1', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); @@ -224,7 +221,7 @@ describe('Exceptions Lists API', () => { describe('deleteExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { @@ -248,14 +245,14 @@ describe('Exceptions Lists API', () => { id: '1', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); describe('deleteExceptionListItemById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionItem); + fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { @@ -279,7 +276,7 @@ describe('Exceptions Lists API', () => { id: '1', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionItem); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx index 098ee1f81f492..b78ad250b8910 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx @@ -6,7 +6,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { mockExceptionItem } from '../mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item'; @@ -32,7 +32,7 @@ describe('usePersistExceptionItem', () => { () => usePersistExceptionItem({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionItem); + result.current[1](getExceptionListItemSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -45,7 +45,7 @@ describe('usePersistExceptionItem', () => { usePersistExceptionItem({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionItem); + result.current[1](getExceptionListItemSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx index 5cad95a38dbec..605dd635aa4f5 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx @@ -6,7 +6,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { mockExceptionList } from '../mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list'; @@ -32,7 +32,7 @@ describe('usePersistExceptionList', () => { () => usePersistExceptionList({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionList); + result.current[1](getExceptionListSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -45,7 +45,7 @@ describe('usePersistExceptionList', () => { usePersistExceptionList({ http: mockKibanaHttpService, onError }) ); await waitForNextUpdate(); - result.current[1](mockExceptionList); + result.current[1](getExceptionListSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/lists/public/exceptions/mock.ts b/x-pack/plugins/lists/public/exceptions/mock.ts index 38a0e65992982..fd06dac65c6fb 100644 --- a/x-pack/plugins/lists/public/exceptions/mock.ts +++ b/x-pack/plugins/lists/public/exceptions/mock.ts @@ -6,27 +6,8 @@ import { CreateExceptionListItemSchemaPartial, CreateExceptionListSchemaPartial, - ExceptionListItemSchema, - ExceptionListSchema, } from '../../common/schemas'; -export const mockExceptionList: ExceptionListSchema = { - _tags: ['endpoint', 'process', 'malware', 'os:linux'], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', - id: '1', - list_id: 'endpoint_list', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', - tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'endpoint', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', -}; - export const mockNewExceptionList: CreateExceptionListSchemaPartial = { _tags: ['endpoint', 'process', 'malware', 'os:linux'], description: 'This is a sample endpoint type exception', @@ -59,36 +40,3 @@ export const mockNewExceptionItem: CreateExceptionListItemSchemaPartial = { tags: ['user added string for a tag', 'malware'], type: 'simple', }; - -export const mockExceptionItem: ExceptionListItemSchema = { - _tags: ['endpoint', 'process', 'malware', 'os:linux'], - comment: [], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', - entries: [ - { - field: 'actingProcess.file.signer', - match: 'Elastic, N.V.', - match_any: undefined, - operator: 'included', - }, - { - field: 'event.category', - match: undefined, - match_any: ['process', 'malware'], - operator: 'included', - }, - ], - id: '1', - item_id: 'endpoint_list_item', - list_id: 'endpoint_list', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', - tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'simple', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', -}; diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index b23f31abd4d87..fb4d5de06ae54 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -7,9 +7,4 @@ export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionList } from './exceptions/hooks/use_exception_list'; -export { - mockExceptionItem, - mockExceptionList, - mockNewExceptionItem, - mockNewExceptionList, -} from './exceptions/mock'; +export { mockNewExceptionItem, mockNewExceptionList } from './exceptions/mock'; diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts index 0992e3c361fcf..a1c78f5ea4684 100644 --- a/x-pack/plugins/lists/server/get_user.test.ts +++ b/x-pack/plugins/lists/server/get_user.test.ts @@ -8,7 +8,6 @@ import { httpServerMock } from 'src/core/server/mocks'; import { KibanaRequest } from 'src/core/server'; import { securityMock } from '../../security/server/mocks'; -import { SecurityPluginSetup } from '../../security/server'; import { getUser } from './get_user'; @@ -24,42 +23,42 @@ describe('get_user', () => { }); test('it returns "bob" as the user given a security request with "bob"', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' }); const user = getUser({ request, security }); expect(user).toEqual('bob'); }); test('it returns "alice" as the user given a security request with "alice"', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' }); const user = getUser({ request, security }); expect(user).toEqual('alice'); }); test('it returns "elastic" as the user given null as the current user', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(null); const user = getUser({ request, security }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given undefined as the current user', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given undefined as the plugin', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security: undefined }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given null as the plugin', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security: null }); expect(user).toEqual('elastic'); diff --git a/x-pack/plugins/lists/server/mocks.ts b/x-pack/plugins/lists/server/mocks.ts new file mode 100644 index 0000000000000..aad4a25a900a1 --- /dev/null +++ b/x-pack/plugins/lists/server/mocks.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListPluginSetup } from './types'; +import { getListClientMock } from './services/lists/list_client.mock'; +import { getExceptionListClientMock } from './services/exception_lists/exception_list_client.mock'; + +const createSetupMock = (): jest.Mocked => { + const mock: jest.Mocked = { + getExceptionListClient: jest.fn().mockReturnValue(getExceptionListClientMock()), + getListClient: jest.fn().mockReturnValue(getListClientMock()), + }; + return mock; +}; + +export const listMock = { + createSetup: createSetupMock, + getExceptionList: getExceptionListClientMock, + getListClient: getListClientMock, +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts new file mode 100644 index 0000000000000..d0e238f8c5c40 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import { getFoundExceptionListSchemaMock } from '../../../common/schemas/response/found_exception_list_schema.mock'; +import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { ExceptionListClient } from './exception_list_client'; + +export class ExceptionListClientMock extends ExceptionListClient { + public getExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public getExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public createExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public updateExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public deleteExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public createExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public updateExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public deleteExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()); + public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock()); +} + +export const getExceptionListClientMock = (): ExceptionListClient => { + const mock = new ExceptionListClientMock({ + savedObjectsClient: savedObjectsClientMock.create(), + user: 'elastic', + }); + return mock; +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts new file mode 100644 index 0000000000000..f91331f5b4308 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { getExceptionListClientMock } from './exception_list_client.mock'; + +describe('exception_list_client', () => { + describe('Mock client sanity checks', () => { + test('it returns the exception list as expected', async () => { + const mock = getExceptionListClientMock(); + const list = await mock.getExceptionList({ + id: '123', + listId: '123', + namespaceType: 'single', + }); + expect(list).toEqual(getExceptionListSchemaMock()); + }); + + test('it returns the the exception list item as expected', async () => { + const mock = getExceptionListClientMock(); + const listItem = await mock.getExceptionListItem({ + id: '123', + itemId: '123', + namespaceType: 'single', + }); + expect(listItem).toEqual(getExceptionListItemSchemaMock()); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts new file mode 100644 index 0000000000000..43a01a3ca62dc --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getFoundListItemSchemaMock } from '../../../common/schemas/response/found_list_item_schema.mock'; +import { getFoundListSchemaMock } from '../../../common/schemas/response/found_list_schema.mock'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + +import { ListClient } from './list_client'; + +export class ListClientMock extends ListClient { + public getListIndex = jest.fn().mockReturnValue(LIST_INDEX); + public getListItemIndex = jest.fn().mockReturnValue(LIST_ITEM_INDEX); + public getList = jest.fn().mockResolvedValue(getListResponseMock()); + public createList = jest.fn().mockResolvedValue(getListResponseMock()); + public createListIfItDoesNotExist = jest.fn().mockResolvedValue(getListResponseMock()); + public getListIndexExists = jest.fn().mockResolvedValue(true); + public getListItemIndexExists = jest.fn().mockResolvedValue(true); + public createListBootStrapIndex = jest.fn().mockResolvedValue({}); + public createListItemBootStrapIndex = jest.fn().mockResolvedValue({}); + public getListPolicyExists = jest.fn().mockResolvedValue(true); + public getListItemPolicyExists = jest.fn().mockResolvedValue(true); + public getListTemplateExists = jest.fn().mockResolvedValue(true); + public getListItemTemplateExists = jest.fn().mockResolvedValue(true); + public getListTemplate = jest.fn().mockResolvedValue({}); + public getListItemTemplate = jest.fn().mockResolvedValue({}); + public setListTemplate = jest.fn().mockResolvedValue({}); + public setListItemTemplate = jest.fn().mockResolvedValue({}); + public setListPolicy = jest.fn().mockResolvedValue({}); + public setListItemPolicy = jest.fn().mockResolvedValue({}); + public deleteListIndex = jest.fn().mockResolvedValue(true); + public deleteListItemIndex = jest.fn().mockResolvedValue(true); + public deleteListPolicy = jest.fn().mockResolvedValue({}); + public deleteListItemPolicy = jest.fn().mockResolvedValue({}); + public deleteListTemplate = jest.fn().mockResolvedValue({}); + public deleteListItemTemplate = jest.fn().mockResolvedValue({}); + public deleteListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public deleteListItemByValue = jest.fn().mockResolvedValue(getListItemResponseMock()); + public deleteList = jest.fn().mockResolvedValue(getListResponseMock()); + public exportListItemsToStream = jest.fn().mockResolvedValue(undefined); + public importListItemsToStream = jest.fn().mockResolvedValue(undefined); + public getListItemByValue = jest.fn().mockResolvedValue([getListItemResponseMock()]); + public createListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public updateListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public updateList = jest.fn().mockResolvedValue(getListResponseMock()); + public getListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public getListItemByValues = jest.fn().mockResolvedValue([getListItemResponseMock()]); + public findList = jest.fn().mockResolvedValue(getFoundListSchemaMock()); + public findListItem = jest.fn().mockResolvedValue(getFoundListItemSchemaMock()); +} + +export const getListClientMock = (): ListClient => { + const mock = new ListClientMock({ + callCluster: getCallClusterMock(), + config: { + enabled: true, + listIndex: LIST_INDEX, + listItemIndex: LIST_ITEM_INDEX, + }, + spaceId: 'default', + user: 'elastic', + }); + return mock; +}; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.test.ts b/x-pack/plugins/lists/server/services/lists/list_client.test.ts new file mode 100644 index 0000000000000..0c3a58283ffe2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_client.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + +import { getListClientMock } from './list_client.mock'; + +describe('list_client', () => { + describe('Mock client sanity checks', () => { + test('it returns the get list index as expected', () => { + const mock = getListClientMock(); + expect(mock.getListIndex()).toEqual(LIST_INDEX); + }); + + test('it returns the get list item index as expected', () => { + const mock = getListClientMock(); + expect(mock.getListItemIndex()).toEqual(LIST_ITEM_INDEX); + }); + + test('it returns a mock list item', async () => { + const mock = getListClientMock(); + const listItem = await mock.getListItem({ id: '123' }); + expect(listItem).toEqual(getListItemResponseMock()); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/types.ts b/x-pack/plugins/lists/server/services/lists/types.ts index 2e0e4b7d038e7..47419929c43a6 100644 --- a/x-pack/plugins/lists/server/services/lists/types.ts +++ b/x-pack/plugins/lists/server/services/lists/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Scroll { +export interface Scroll { searchAfter: string[] | undefined; validSearchAfterFound: boolean; } diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts index 9721baefbe5ee..2af501106d659 100644 --- a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -7,6 +7,7 @@ import { APICaller } from 'kibana/server'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; +import { Scroll } from '../lists/types'; import { getQueryFilter } from './get_query_filter'; import { getSortWithTieBreaker } from './get_sort_with_tie_breaker'; diff --git a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts index 16e07044dc0d4..6b898a54bb9fe 100644 --- a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts +++ b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts @@ -7,6 +7,7 @@ import { APICaller } from 'kibana/server'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; +import { Scroll } from '../lists/types'; import { calculateScrollMath } from './calculate_scroll_math'; import { getSearchAfterScroll } from './get_search_after_scroll'; diff --git a/x-pack/plugins/siem/public/lists_plugin_deps.ts b/x-pack/plugins/siem/public/lists_plugin_deps.ts index b99e5015c716a..d2ee5ae56b7d9 100644 --- a/x-pack/plugins/siem/public/lists_plugin_deps.ts +++ b/x-pack/plugins/siem/public/lists_plugin_deps.ts @@ -8,8 +8,6 @@ export { useExceptionList, usePersistExceptionItem, usePersistExceptionList, - mockExceptionItem, - mockExceptionList, mockNewExceptionItem, mockNewExceptionList, } from '../../lists/public'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 86efdb6603493..d56e167f59e4c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -8,17 +8,23 @@ import uuid from 'uuid'; import { filterEventsAgainstList } from './filter_events_with_list'; import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results'; -import { ListClient } from '../../../../../lists/server'; +import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; +import { listMock } from '../../../../../lists/server/mocks'; const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); describe('filterEventsAgainstList', () => { + let listClient = listMock.getListClient(); + beforeEach(() => { + jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.getListItemByValues = jest.fn().mockResolvedValue([]); + }); + it('should respond with eventSearchResult if exceptionList is empty', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: undefined, eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', @@ -35,9 +41,7 @@ describe('filterEventsAgainstList', () => { try { await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -66,9 +70,7 @@ describe('filterEventsAgainstList', () => { try { await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -101,9 +103,7 @@ describe('filterEventsAgainstList', () => { it('should respond with same list if no items match value list', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -122,27 +122,17 @@ describe('filterEventsAgainstList', () => { expect(res.hits.hits.length).toEqual(4); }); it('should respond with less items in the list if some values match', async () => { - let outerType = ''; - let outerListId = ''; + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async ({ - value, - type, - listId, - }: { - type: string; - listId: string; - value: string[]; - }) => { - outerType = type; - outerListId = listId; - return value.slice(0, 2).map((item) => ({ - value: item, - })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -163,8 +153,10 @@ describe('filterEventsAgainstList', () => { '7.7.7.7', ]), }); - expect(outerType).toEqual('ip'); - expect(outerListId).toEqual('ci-badguys.txt'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( + 'ci-badguys.txt' + ); expect(res.hits.hits.length).toEqual(2); }); }); @@ -172,9 +164,7 @@ describe('filterEventsAgainstList', () => { it('should respond with empty list if no items match value list', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -193,27 +183,17 @@ describe('filterEventsAgainstList', () => { expect(res.hits.hits.length).toEqual(0); }); it('should respond with less items in the list if some values match', async () => { - let outerType = ''; - let outerListId = ''; + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const res = await filterEventsAgainstList({ logger: mockLogger, - listClient: ({ - getListItemByValues: async ({ - value, - type, - listId, - }: { - type: string; - listId: string; - value: string[]; - }) => { - outerType = type; - outerListId = listId; - return value.slice(0, 2).map((item) => ({ - value: item, - })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -234,8 +214,10 @@ describe('filterEventsAgainstList', () => { '7.7.7.7', ]), }); - expect(outerType).toEqual('ip'); - expect(outerListId).toEqual('ci-badguys.txt'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( + 'ci-badguys.txt' + ); expect(res.hits.hits.length).toEqual(2); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 7479ab54af6e6..a306a016b4205 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -15,15 +15,18 @@ import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import uuid from 'uuid'; -import { ListClient } from '../../../../../lists/server'; -import { ListItemArraySchema } from '../../../../../lists/common/schemas'; +import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; +import { listMock } from '../../../../../lists/server/mocks'; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; let inputIndexPattern: string[] = []; + let listClient = listMock.getListClient(); const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); beforeEach(() => { jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.getListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createAlertServices(); }); @@ -93,9 +96,7 @@ describe('searchAfterAndBulkCreate', () => { }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -169,9 +170,7 @@ describe('searchAfterAndBulkCreate', () => { }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - listClient: ({ - getListItemByValues: async () => [], - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -243,19 +242,18 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); + + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - listClient: ({ - getListItemByValues: async ({ - value, - }: { - type: string; - listId: string; - value: string[]; - }) => { - return value.map((item) => ({ value: item })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: undefined, services: mockService, logger: mockLogger, @@ -288,11 +286,7 @@ describe('searchAfterAndBulkCreate', () => { .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockRejectedValue(new Error('bulk failed')); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient: ({ - getListItemByValues: async () => { - return ([] as unknown) as ListItemArraySchema; - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -335,18 +329,16 @@ describe('searchAfterAndBulkCreate', () => { test('should return success with 0 total hits', async () => { const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient: ({ - getListItemByValues: async ({ - value, - }: { - type: string; - listId: string; - value: string[]; - }) => { - return value.map((item) => ({ value: item })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', @@ -405,18 +397,16 @@ describe('searchAfterAndBulkCreate', () => { .mockImplementation(() => { throw Error('Fake Error'); }); + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - listClient: ({ - getListItemByValues: async ({ - value, - }: { - type: string; - listId: string; - value: string[]; - }) => { - return value.map((item) => ({ value: item })); - }, - } as unknown) as ListClient, + listClient, exceptionsList: [ { field: 'source.ip', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index 05cdccedbc2c1..59c685ec3e815 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -81,7 +81,7 @@ export const searchAfterAndBulkCreate = async ({ let sortId; // tells us where to start our next search_after query let searchResultSize = 0; - /* + /* The purpose of `maxResults` is to ensure we do not perform extra search_after's. This will be reset on each iteration, although it really only matters for the first diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index ea7255b8a925a..8e7034b006327 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -17,7 +17,7 @@ import { scheduleNotificationActions } from '../notifications/schedule_notificat import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; -import { ListPluginSetup } from '../../../../../lists/server/types'; +import { listMock } from '../../../../../lists/server/mocks'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -69,11 +69,6 @@ describe('rules_notification_alert_type', () => { modulesProvider: jest.fn(), resultsServiceProvider: jest.fn(), }; - const listMock = { - getListClient: () => ({ - getListItemByValues: () => [], - }), - }; let payload: jest.Mocked; let alert: ReturnType; let logger: ReturnType; @@ -116,7 +111,7 @@ describe('rules_notification_alert_type', () => { logger, version, ml: mlMock, - lists: (listMock as unknown) as ListPluginSetup, + lists: listMock.createSetup(), }); }); From 6753b1d36bc567a8948f24d600fd439b8125acb8 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 1 Jun 2020 14:34:53 +0200 Subject: [PATCH 20/69] Added autocompletion for update by query (#67741) Co-authored-by: Elastic Machine --- .../json/overrides/update_by_query.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json new file mode 100644 index 0000000000000..44819eda6e29e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json @@ -0,0 +1,17 @@ +{ + "update_by_query": { + "data_autocomplete_rules": { + "conflicts": "", + "query": { + "__scope_link": "GLOBAL.query" + }, + "script": { + "__template": { + "source": "", + "lang": "painless" + }, + "__scope_link": "GLOBAL.script" + } + } + } +} From df4615a392aa830eed4811f33478729a2df2405f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 1 Jun 2020 08:43:36 -0400 Subject: [PATCH 21/69] [ILM] Fix fetch policies query (#67827) --- .../public/application/services/api.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js index 386df63111a89..6b46d6e6ea735 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js @@ -28,8 +28,7 @@ export async function loadIndexTemplates() { } export async function loadPolicies(withIndices) { - const query = withIndices ? '?withIndices=true' : ''; - return await sendGet('policies', query); + return await sendGet('policies', { withIndices }); } export async function savePolicy(policy) { From 773a44defa91d4cfdcd209760d76f9d715eafe3f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 1 Jun 2020 08:44:38 -0400 Subject: [PATCH 22/69] [Component templates] Server side (#66596) --- .../server/client/elasticsearch.ts | 63 ++++ .../plugins/index_management/server/plugin.ts | 49 ++- .../routes/api/component_templates/create.ts | 79 +++++ .../routes/api/component_templates/delete.ts | 51 +++ .../routes/api/component_templates/get.ts | 77 +++++ .../routes/api/component_templates/index.ts | 19 ++ .../component_templates/schema_validation.ts | 16 + .../routes/api/component_templates/update.ts | 62 ++++ .../index_management/server/routes/index.ts | 2 + .../server/services/license.ts | 4 +- .../index_management/component_templates.ts | 296 ++++++++++++++++++ .../apis/management/index_management/index.js | 1 + .../index_management/lib/elasticsearch.js | 10 + .../api_integration/services/legacy_es.js | 5 +- 14 files changed, 727 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/index_management/server/client/elasticsearch.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/create.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/get.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/index.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/update.ts create mode 100644 x-pack/test/api_integration/apis/management/index_management/component_templates.ts diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts new file mode 100644 index 0000000000000..65bd5411a249b --- /dev/null +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { + const ca = components.clientAction.factory; + + Client.prototype.dataManagement = components.clientAction.namespaceFactory(); + const dataManagement = Client.prototype.dataManagement.prototype; + + dataManagement.getComponentTemplates = ca({ + urls: [ + { + fmt: '/_component_template', + }, + ], + method: 'GET', + }); + + dataManagement.getComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + dataManagement.saveComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'PUT', + }); + + dataManagement.deleteComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); +}; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index e5bd7451b028f..f254333007c39 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -3,14 +3,33 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +declare module 'kibana/server' { + interface RequestHandlerContext { + dataManagement?: DataManagementContext; + } +} + import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server'; +import { + CoreSetup, + Plugin, + Logger, + PluginInitializerContext, + IScopedClusterClient, + ICustomClusterClient, +} from 'src/core/server'; import { PLUGIN } from '../common'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { License, IndexDataEnricher } from './services'; import { isEsError } from './lib/is_es_error'; +import { elasticsearchJsPlugin } from './client/elasticsearch'; + +export interface DataManagementContext { + client: IScopedClusterClient; +} export interface IndexManagementPluginSetup { indexDataEnricher: { @@ -18,11 +37,18 @@ export interface IndexManagementPluginSetup { }; } +async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { + const [core] = await getStartServices(); + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + return core.elasticsearch.legacy.createClient('dataManagement', esClientConfig); +} + export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly license: License; private readonly logger: Logger; private readonly indexDataEnricher: IndexDataEnricher; + private dataManagementESClient?: ICustomClusterClient; constructor(initContext: PluginInitializerContext) { this.logger = initContext.logger.get(); @@ -31,7 +57,10 @@ export class IndexMgmtServerPlugin implements Plugin { + this.dataManagementESClient = + this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); + + return { + client: this.dataManagementESClient.asScoped(request), + }; + }); + this.apiRoutes.setup({ router, license: this.license, @@ -65,5 +103,10 @@ export class IndexMgmtServerPlugin implements Plugin { + router.post( + { + path: addBasePath('/component_templates'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + const { name, ...componentTemplateDefinition } = req.body; + + try { + // Check that a component template with the same name doesn't already exist + const componentTemplateResponse = await callAsCurrentUser( + 'dataManagement.getComponentTemplate', + { name } + ); + + const { component_templates: componentTemplates } = componentTemplateResponse; + + if (componentTemplates.length) { + return res.conflict({ + body: new Error( + i18n.translate('xpack.idxMgmt.componentTemplates.createRoute.duplicateErrorMessage', { + defaultMessage: "There is already a component template with name '{name}'.", + values: { + name, + }, + }) + ), + }); + } + } catch (e) { + // Silently swallow error + } + + try { + const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + name, + body: componentTemplateDefinition, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts new file mode 100644 index 0000000000000..9e11967202b9c --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const paramsSchema = schema.object({ + names: schema.string(), +}); + +export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { + router.delete( + { + path: addBasePath('/component_templates/{names}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { names } = req.params; + const componentNames = names.split(','); + + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + componentNames.map((componentName) => { + return callAsCurrentUser('dataManagement.deleteComponentTemplate', { + name: componentName, + }) + .then(() => response.itemsDeleted.push(componentName)) + .catch((e) => + response.errors.push({ + name: componentName, + error: e, + }) + ); + }) + ); + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts new file mode 100644 index 0000000000000..87aa64421624e --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { + // Get all component templates + router.get( + { path: addBasePath('/component_templates'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + try { + const response = await callAsCurrentUser('dataManagement.getComponentTemplates'); + + return res.ok({ body: response.component_templates }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); + + // Get single component template + router.get( + { + path: addBasePath('/component_templates/{name}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { name } = req.params; + + try { + const { component_templates: componentTemplates } = await callAsCurrentUser( + 'dataManagement.getComponentTemplates', + { + name, + } + ); + + return res.ok({ + body: { + ...componentTemplates[0], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts new file mode 100644 index 0000000000000..7ecb71182e87e --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; + +import { registerGetAllRoute } from './get'; +import { registerCreateRoute } from './create'; +import { registerUpdateRoute } from './update'; +import { registerDeleteRoute } from './delete'; + +export function registerComponentTemplateRoutes(dependencies: RouteDependencies) { + registerGetAllRoute(dependencies); + registerCreateRoute(dependencies); + registerUpdateRoute(dependencies); + registerDeleteRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts new file mode 100644 index 0000000000000..7d32637c6b977 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +export const componentTemplateSchema = { + template: schema.object({ + settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), + mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }), + version: schema.maybe(schema.number()), + _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts new file mode 100644 index 0000000000000..7e447bb110c67 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; +import { componentTemplateSchema } from './schema_validation'; + +const bodySchema = schema.object(componentTemplateSchema); + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: addBasePath('/component_templates/{name}'), + validate: { + body: bodySchema, + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { name } = req.params; + const { template, version, _meta } = req.body; + + try { + // Verify component exists; ES will throw 404 if not + await callAsCurrentUser('dataManagement.getComponentTemplate', { name }); + + const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + name, + body: { + template, + version, + _meta, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index 870cfa36ecc6a..1e5aaf8087624 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -11,6 +11,7 @@ import { registerTemplateRoutes } from './api/templates'; import { registerMappingRoute } from './api/mapping'; import { registerSettingsRoutes } from './api/settings'; import { registerStatsRoute } from './api/stats'; +import { registerComponentTemplateRoutes } from './api/component_templates'; export class ApiRoutes { setup(dependencies: RouteDependencies) { @@ -19,6 +20,7 @@ export class ApiRoutes { registerSettingsRoutes(dependencies); registerStatsRoute(dependencies); registerMappingRoute(dependencies); + registerComponentTemplateRoutes(dependencies); } start() {} diff --git a/x-pack/plugins/index_management/server/services/license.ts b/x-pack/plugins/index_management/server/services/license.ts index 2d863e283d440..9b68acd073c4a 100644 --- a/x-pack/plugins/index_management/server/services/license.ts +++ b/x-pack/plugins/index_management/server/services/license.ts @@ -53,12 +53,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = license.getStatus(); diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts new file mode 100644 index 0000000000000..a33e82ad9f79d --- /dev/null +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +// @ts-ignore +import { initElasticsearchHelpers } from './lib'; +// @ts-ignore +import { API_BASE_PATH } from './constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + const { createComponentTemplate, deleteComponentTemplate } = initElasticsearchHelpers(es); + + describe('Component templates', function () { + describe('Get', () => { + const COMPONENT_NAME = 'test_component_template'; + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }; + + before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); + after(() => deleteComponentTemplate(COMPONENT_NAME)); + + describe('all component templates', () => { + it('should return an array of component templates', async () => { + const { body: componentTemplates } = await supertest + .get(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + const testComponentTemplate = componentTemplates.find( + ({ name }: { name: string }) => name === COMPONENT_NAME + ); + + expect(testComponentTemplate).to.eql({ + name: COMPONENT_NAME, + component_template: COMPONENT, + }); + }); + }); + + describe('one component template', () => { + it('should return a single component template', async () => { + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + name: COMPONENT_NAME, + component_template: { + ...COMPONENT, + }, + }); + }); + }); + }); + + describe('Create', () => { + const COMPONENT_NAME = 'test_create_component_template'; + const REQUIRED_FIELDS_COMPONENT_NAME = 'test_create_required_fields_component_template'; + + after(() => { + deleteComponentTemplate(COMPONENT_NAME); + deleteComponentTemplate(REQUIRED_FIELDS_COMPONENT_NAME); + }); + + it('should create a component template', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .send({ + name: COMPONENT_NAME, + version: 1, + template: { + settings: { + number_of_shards: 1, + }, + aliases: { + alias1: {}, + }, + mappings: { + properties: { + host_name: { + type: 'keyword', + }, + }, + }, + }, + _meta: { + description: 'set number of shards to one', + serialization: { + class: 'MyComponentTemplate', + id: 10, + }, + }, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should create a component template with only required fields', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + // Excludes version and _meta fields + .send({ + name: REQUIRED_FIELDS_COMPONENT_NAME, + template: {}, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of a component template with the same name of an existing one', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .send({ + name: COMPONENT_NAME, + template: {}, + }) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a component template with name '${COMPONENT_NAME}'.`, + }); + }); + }); + + describe('Update', () => { + const COMPONENT_NAME = 'test_component_template'; + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }; + + before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); + after(() => deleteComponentTemplate(COMPONENT_NAME)); + + it('should allow an existing component template to be updated', async () => { + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...COMPONENT, + version: 1, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing component template to be updated', async () => { + const uri = `${API_BASE_PATH}/component_templates/component_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...COMPONENT, + version: 1, + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: + '[resource_not_found_exception] component template matching [component_does_not_exist] not found', + }); + }); + }); + + describe('Delete', () => { + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + }, + }; + + it('should delete a component template', async () => { + // Create component template to be deleted + const COMPONENT_NAME = 'test_delete_component_template'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + itemsDeleted: [COMPONENT_NAME], + errors: [], + }); + }); + + it('should delete multiple component templates', async () => { + // Create component templates to be deleted + const COMPONENT_ONE_NAME = 'test_delete_component_1'; + const COMPONENT_TWO_NAME = 'test_delete_component_2'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); + createComponentTemplate({ body: COMPONENT, name: COMPONENT_TWO_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_TWO_NAME}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each name instead + [COMPONENT_ONE_NAME, COMPONENT_TWO_NAME].forEach((componentName) => { + expect(itemsDeleted.includes(componentName)).to.be(true); + }); + }); + + it('should return an error for any component templates not sucessfully deleted', async () => { + const COMPONENT_DOES_NOT_EXIST = 'component_does_not_exist'; + + // Create component template to be deleted + const COMPONENT_ONE_NAME = 'test_delete_component_1'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_DOES_NOT_EXIST}`; + + const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body.itemsDeleted).to.eql([COMPONENT_ONE_NAME]); + expect(body.errors[0].name).to.eql(COMPONENT_DOES_NOT_EXIST); + expect(body.errors[0].error.msg).to.contain('index_template_missing_exception'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/index_management/index.js b/x-pack/test/api_integration/apis/management/index_management/index.js index cd3e27f9f7a61..fdee325938ff4 100644 --- a/x-pack/test/api_integration/apis/management/index_management/index.js +++ b/x-pack/test/api_integration/apis/management/index_management/index.js @@ -11,5 +11,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./templates')); + loadTestFile(require.resolve('./component_templates')); }); } diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js index 78aed8142eeba..b950a56a913db 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js @@ -34,6 +34,14 @@ export const initElasticsearchHelpers = (es) => { const catTemplate = (name) => es.cat.templates({ name, format: 'json' }); + const createComponentTemplate = (componentTemplate) => { + return es.dataManagement.saveComponentTemplate(componentTemplate); + }; + + const deleteComponentTemplate = (componentTemplateName) => { + return es.dataManagement.deleteComponentTemplate({ name: componentTemplateName }); + }; + return { createIndex, deleteIndex, @@ -42,5 +50,7 @@ export const initElasticsearchHelpers = (es) => { indexStats, cleanUp, catTemplate, + createComponentTemplate, + deleteComponentTemplate, }; }; diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 12a1576f78982..0ea061365aca2 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -8,7 +8,8 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import { elasticsearchClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; +import { elasticsearchClientPlugin as securityEsClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; +import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -19,6 +20,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [elasticsearchClientPlugin], + plugins: [securityEsClientPlugin, indexManagementEsClientPlugin], }); } From afbbafb0cfb556cda333d0b58dd81e8e424a5a6e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 1 Jun 2020 09:36:52 -0400 Subject: [PATCH 23/69] Fix support for `xpack.spaces.maxSpaces` (#67846) --- x-pack/legacy/plugins/spaces/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 2f3e5e0a86d21..79c57e564b4e1 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -17,7 +17,13 @@ export const spaces = (kibana: Record) => configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }) + .unknown() + .default(); + }, uiExports: { managementSections: [], apps: [], From 53b95424fec89bf503390bcafb5a62e04b28801b Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 1 Jun 2020 17:16:44 +0300 Subject: [PATCH 24/69] Deprecate es API exposed from setup contract (#67596) * move elasticsearch client under legacy namespace * update mocks and tests * update platform code * update legacy code * update plugins using elasticsearch setup API * update request handler context * update docs * rename remaining places * address comments * fix merge conflict error --- ...r.elasticsearchservicesetup.adminclient.md | 27 ----- ....elasticsearchservicesetup.createclient.md | 28 ------ ...er.elasticsearchservicesetup.dataclient.md | 27 ----- ...server.elasticsearchservicesetup.legacy.md | 19 ++++ ...n-core-server.elasticsearchservicesetup.md | 4 +- ...server.elasticsearchservicestart.legacy.md | 5 + .../core/server/kibana-plugin-core-server.md | 2 +- ...lugin-core-server.requesthandlercontext.md | 2 +- .../elasticsearch_service.mock.ts | 47 ++++----- .../elasticsearch_service.test.ts | 80 ++++++--------- .../elasticsearch/elasticsearch_service.ts | 74 +++++--------- src/core/server/elasticsearch/types.ts | 98 +++++++++---------- .../integration_tests/core_services.test.ts | 8 +- src/core/server/index.ts | 4 +- src/core/server/legacy/legacy_service.ts | 7 +- src/core/server/mocks.ts | 1 + src/core/server/plugins/plugin_context.ts | 4 +- .../saved_objects_service.test.ts | 47 +++++---- .../saved_objects/saved_objects_service.ts | 30 ++++-- src/core/server/server.api.md | 11 +-- src/core/server/server.ts | 3 +- .../core_plugins/elasticsearch/index.js | 8 +- .../saved_objects/saved_objects_mixin.test.js | 7 -- src/plugins/telemetry/server/plugin.ts | 2 +- .../csv/server/execute_job.test.ts | 6 +- .../export_types/csv/server/execute_job.ts | 2 +- .../server/lib/generate_csv_search.ts | 2 +- .../png/server/execute_job/index.test.ts | 6 +- .../server/execute_job/index.test.ts | 6 +- .../reporting/server/lib/create_queue.ts | 2 +- .../reporting/server/lib/jobs_query.ts | 2 +- .../validate_max_content_length.test.js | 32 +++--- .../validate/validate_max_content_length.ts | 2 +- .../server/routes/generation.test.ts | 4 +- .../reporting/server/routes/jobs.test.ts | 22 ++--- x-pack/plugins/licensing/server/plugin.ts | 8 +- x-pack/plugins/ml/server/plugin.ts | 2 +- .../server/kibana_monitoring/bulk_uploader.js | 6 +- x-pack/plugins/monitoring/server/plugin.ts | 4 +- .../server/routes/api/add_route.test.ts | 2 +- .../server/routes/api/delete_route.test.ts | 2 +- .../server/routes/api/get_route.test.ts | 2 +- .../server/routes/api/update_route.test.ts | 2 +- x-pack/plugins/security/server/plugin.test.ts | 8 +- x-pack/plugins/security/server/plugin.ts | 2 +- .../server/plugin.ts | 2 +- .../plugins/alerts/server/action_types.ts | 4 +- .../plugins/alerts/server/alert_types.ts | 4 +- .../sample_task_plugin/server/init_routes.ts | 4 +- .../sample_task_plugin/server/plugin.ts | 4 +- 50 files changed, 292 insertions(+), 395 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md deleted file mode 100644 index 3fcb855586129..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [adminClient](./kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md) - -## ElasticsearchServiceSetup.adminClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.client](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly adminClient: IClusterClient; -``` - -## Example - - -```js -const client = core.elasticsearch.adminClient; - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md deleted file mode 100644 index 75bf6c6aa461b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicesetup.createclient.md) - -## ElasticsearchServiceSetup.createClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.createClient](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; -``` - -## Example - - -```js -const client = elasticsearch.createCluster('my-app-name', config); -const data = await client.callAsInternalUser(); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md deleted file mode 100644 index 867cafa957f42..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [dataClient](./kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md) - -## ElasticsearchServiceSetup.dataClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.client](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly dataClient: IClusterClient; -``` - -## Example - - -```js -const client = core.elasticsearch.dataClient; - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md new file mode 100644 index 0000000000000..e8c4c63dc6a96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) + +## ElasticsearchServiceSetup.legacy property + +> Warning: This API is now obsolete. +> +> Use [ElasticsearchServiceStart.legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. +> + +Signature: + +```typescript +legacy: { + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; + readonly client: IClusterClient; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md index ee56f8b4a6284..c1e23527e9516 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md @@ -15,7 +15,5 @@ export interface ElasticsearchServiceSetup | Property | Type | Description | | --- | --- | --- | -| [adminClient](./kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md) | IClusterClient | | -| [createClient](./kibana-plugin-core-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | | -| [dataClient](./kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md) | IClusterClient | | +| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | {
    readonly createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient;
    readonly client: IClusterClient;
    } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md index 08765aaf93d3d..667a36091f232 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md @@ -4,6 +4,11 @@ ## ElasticsearchServiceStart.legacy property +> Warning: This API is now obsolete. +> +> Provided for the backward compatibility. Switch to the new elasticsearch client as soon as https://github.com/elastic/kibana/issues/35508 done. +> + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 14e01fda3d287..147a72016b235 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -125,7 +125,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 6b3fc4c03ec73..99be0676bcda3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index a7d78b56ff3fd..55e60f5987604 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -23,12 +23,7 @@ import { IClusterClient, ICustomClusterClient } from './cluster_client'; import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; -import { - InternalElasticsearchServiceSetup, - ElasticsearchServiceSetup, - ElasticsearchServiceStart, - ElasticsearchStatusMeta, -} from './types'; +import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; @@ -51,32 +46,26 @@ function createClusterClientMock() { return client; } -type MockedElasticSearchServiceSetup = jest.Mocked< - ElasticsearchServiceSetup & { - adminClient: jest.Mocked; - dataClient: jest.Mocked; - } ->; +interface MockedElasticSearchServiceSetup { + legacy: { + createClient: jest.Mock; + client: jest.Mocked; + }; +} const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { - createClient: jest.fn(), - adminClient: createClusterClientMock(), - dataClient: createClusterClientMock(), + legacy: { + createClient: jest.fn(), + client: createClusterClientMock(), + }, }; - setupContract.createClient.mockReturnValue(createCustomClusterClientMock()); - setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); - setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.createClient.mockReturnValue(createCustomClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; -type MockedElasticSearchServiceStart = { - legacy: jest.Mocked; -} & { - legacy: { - client: jest.Mocked; - }; -}; +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { @@ -92,13 +81,11 @@ const createStartContractMock = () => { type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { - adminClient: jest.Mocked; - dataClient: jest.Mocked; + legacy: { client: jest.Mocked }; } >; const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { - ...createSetupContractMock(), esNodesCompatibility$: new BehaviorSubject({ isCompatible: true, incompatibleNodes: [], @@ -111,10 +98,10 @@ const createInternalSetupContractMock = () => { }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), + ...createSetupContractMock().legacy, }, }; - setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); - setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 26144eaaa4afa..e7dab3807733a 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -74,25 +74,16 @@ describe('#setup', () => { ); }); - it('returns data and admin client as a part of the contract', async () => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + it('returns elasticsearch client as a part of the contract', async () => { + const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); const setupContract = await elasticsearchService.setup(deps); + const client = setupContract.legacy.client; - const adminClient = setupContract.adminClient; - const dataClient = setupContract.dataClient; - - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await adminClient.callAsInternalUser('any'); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); - - expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await dataClient.callAsInternalUser('any'); - expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + await client.callAsInternalUser('any'); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); }); describe('#createClient', () => { @@ -103,7 +94,7 @@ describe('#setup', () => { MockClusterClient.mockImplementation(() => mockClusterClientInstance); const customConfig = { logQueries: true }; - const clusterClient = setupContract.createClient('some-custom-type', customConfig); + const clusterClient = setupContract.legacy.createClient('some-custom-type', customConfig); expect(clusterClient).toBe(mockClusterClientInstance); @@ -124,7 +115,7 @@ describe('#setup', () => { logQueries: true, ssl: { certificate: 'certificate-value' }, }; - setupContract.createClient('some-custom-type', customConfig); + setupContract.legacy.createClient('some-custom-type', customConfig); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -149,7 +140,7 @@ describe('#setup', () => { // reset all mocks called during setup phase MockClusterClient.mockClear(); - setupContract.createClient('another-type'); + setupContract.legacy.createClient('another-type'); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -195,7 +186,7 @@ describe('#setup', () => { logQueries: true, ssl: { certificate: 'certificate-value' }, }; - setupContract.createClient('some-custom-type', customConfig); + setupContract.legacy.createClient('some-custom-type', customConfig); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -218,40 +209,34 @@ describe('#setup', () => { }); it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const clusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => clusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + clusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); await delay(10); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); setupContract.esNodesCompatibility$.subscribe(() => { - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async (done) => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); const sub = setupContract.esNodesCompatibility$.subscribe(async () => { sub.unsubscribe(); await delay(100); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); @@ -259,38 +244,31 @@ describe('#setup', () => { describe('#stop', () => { it('stops both admin and data clients', async () => { - const mockAdminClusterClientInstance = { close: jest.fn() }; - const mockDataClusterClientInstance = { close: jest.fn() }; - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const mockClusterClientInstance = { close: jest.fn() }; + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); await elasticsearchService.setup(deps); await elasticsearchService.stop(); - expect(mockAdminClusterClientInstance.close).toHaveBeenCalledTimes(1); - expect(mockDataClusterClientInstance.close).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.close).toHaveBeenCalledTimes(1); }); it('stops pollEsNodeVersions even if there are active subscriptions', async (done) => { expect.assertions(2); - const mockAdminClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); + const mockClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); setupContract.esNodesCompatibility$.subscribe(async () => { - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); await elasticsearchService.stop(); await delay(100); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index ab9c9e11fedc8..26001bf83924f 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -50,8 +50,7 @@ import { calculateStatus$ } from './status'; /** @internal */ interface CoreClusterClients { config: ElasticsearchConfig; - adminClient: ClusterClient; - dataClient: ClusterClient; + client: ClusterClient; } interface SetupDeps { @@ -70,7 +69,7 @@ export class ElasticsearchService type: string, clientConfig?: Partial ) => ICustomClusterClient; - private adminClient?: IClusterClient; + private client?: IClusterClient; constructor(private readonly coreContext: CoreContext) { this.kibanaVersion = coreContext.env.packageInfo.version; @@ -95,21 +94,19 @@ export class ElasticsearchService switchMap( (config) => new Observable((subscriber) => { - this.log.debug(`Creating elasticsearch clients`); + this.log.debug('Creating elasticsearch client'); const coreClients = { config, - adminClient: this.createClusterClient('admin', config), - dataClient: this.createClusterClient('data', config, deps.http.getAuthHeaders), + client: this.createClusterClient('data', config, deps.http.getAuthHeaders), }; subscriber.next(coreClients); return () => { - this.log.debug(`Closing elasticsearch clients`); + this.log.debug('Closing elasticsearch client'); - coreClients.adminClient.close(); - coreClients.dataClient.close(); + coreClients.client.close(); }; }) ), @@ -120,54 +117,27 @@ export class ElasticsearchService const config = await this.config$.pipe(first()).toPromise(); - const adminClient$ = clients$.pipe(map((clients) => clients.adminClient)); - const dataClient$ = clients$.pipe(map((clients) => clients.dataClient)); + const client$ = clients$.pipe(map((clients) => clients.client)); - this.adminClient = { + const client = { async callAsInternalUser( endpoint: string, clientParams: Record = {}, options?: CallAPIOptions ) { - const client = await adminClient$.pipe(take(1)).toPromise(); - return await client.callAsInternalUser(endpoint, clientParams, options); - }, - asScoped: (request: ScopeableRequest) => { - return { - callAsInternalUser: this.adminClient!.callAsInternalUser, - async callAsCurrentUser( - endpoint: string, - clientParams: Record = {}, - options?: CallAPIOptions - ) { - const client = await adminClient$.pipe(take(1)).toPromise(); - return await client - .asScoped(request) - .callAsCurrentUser(endpoint, clientParams, options); - }, - }; - }, - }; - - const dataClient = { - async callAsInternalUser( - endpoint: string, - clientParams: Record = {}, - options?: CallAPIOptions - ) { - const client = await dataClient$.pipe(take(1)).toPromise(); - return await client.callAsInternalUser(endpoint, clientParams, options); + const _client = await client$.pipe(take(1)).toPromise(); + return await _client.callAsInternalUser(endpoint, clientParams, options); }, asScoped(request: ScopeableRequest) { return { - callAsInternalUser: dataClient.callAsInternalUser, + callAsInternalUser: client.callAsInternalUser, async callAsCurrentUser( endpoint: string, clientParams: Record = {}, options?: CallAPIOptions ) { - const client = await dataClient$.pipe(take(1)).toPromise(); - return await client + const _client = await client$.pipe(take(1)).toPromise(); + return await _client .asScoped(request) .callAsCurrentUser(endpoint, clientParams, options); }, @@ -175,8 +145,10 @@ export class ElasticsearchService }, }; + this.client = client; + const esNodesCompatibility$ = pollEsNodesVersion({ - callWithInternalUser: this.adminClient.callAsInternalUser, + callWithInternalUser: client.callAsInternalUser, log: this.log, ignoreVersionMismatch: config.ignoreVersionMismatch, esVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), @@ -189,22 +161,22 @@ export class ElasticsearchService }; return { - legacy: { config$: clients$.pipe(map((clients) => clients.config)) }, + legacy: { + config$: clients$.pipe(map((clients) => clients.config)), + client, + createClient: this.createClient, + }, esNodesCompatibility$, - adminClient: this.adminClient, - dataClient, - createClient: this.createClient, status$: calculateStatus$(esNodesCompatibility$), }; } - public async start() { - if (typeof this.adminClient === 'undefined' || typeof this.createClient === 'undefined') { + if (typeof this.client === 'undefined' || typeof this.createClient === 'undefined') { throw new Error('ElasticsearchService needs to be setup before calling start'); } else { return { legacy: { - client: this.adminClient, + client: this.client, createClient: this.createClient, }, }; diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 3d38935e9fbf0..6fef08fc298ff 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -30,62 +30,60 @@ import { ServiceStatus } from '../status'; export interface ElasticsearchServiceSetup { /** * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. + * Use {@link ElasticsearchServiceStart.legacy} instead. * - * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createCluster('my-app-name', config); - * const data = await client.callAsInternalUser(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient; - - /** - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. - * See {@link IClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.adminClient; - * ``` - */ - readonly adminClient: IClusterClient; + * */ + legacy: { + /** + * @deprecated + * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. + * + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createCluster('my-app-name', config); + * const data = await client.callAsInternalUser(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; - /** - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. - * See {@link IClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.dataClient; - * ``` - */ - readonly dataClient: IClusterClient; + /** + * @deprecated + * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. + * + * All Elasticsearch config value changes are processed under the hood. + * See {@link IClusterClient}. + * + * @example + * ```js + * const client = core.elasticsearch.legacy.client; + * ``` + */ + readonly client: IClusterClient; + }; } /** * @public */ export interface ElasticsearchServiceStart { + /** + * @deprecated + * Provided for the backward compatibility. + * Switch to the new elasticsearch client as soon as https://github.com/elastic/kibana/issues/35508 done. + * */ legacy: { /** * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. @@ -123,9 +121,9 @@ export interface ElasticsearchServiceStart { } /** @internal */ -export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup { +export interface InternalElasticsearchServiceSetup { // Required for the BWC with the legacy Kibana only. - readonly legacy: { + readonly legacy: ElasticsearchServiceSetup['legacy'] & { readonly config$: Observable; }; esNodesCompatibility$: Observable; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 9583cca177619..ba39effa77016 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -383,8 +383,8 @@ describe('http service', () => { // client contains authHeaders for BWC with legacy platform. const [client] = clusterClientMock.mock.calls; - const [, , dataClientHeaders] = client; - expect(dataClientHeaders).toEqual(authHeaders); + const [, , clientHeaders] = client; + expect(clientHeaders).toEqual(authHeaders); }); it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { @@ -407,8 +407,8 @@ describe('http service', () => { .expect(200); const [client] = clusterClientMock.mock.calls; - const [, , dataClientHeaders] = client; - expect(dataClientHeaders).toEqual({ authorization: authorizationHeader }); + const [, , clientHeaders] = client; + expect(clientHeaders).toEqual({ authorization: authorizationHeader }); }); }); }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 96bb0c9a006b0..658c24f835020 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -338,10 +338,8 @@ export { * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing * all the registered types. - * - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch + * - {@link ScopedClusterClient | elasticsearch.legacy.client} - Elasticsearch * data client which uses the credentials of the incoming request - * - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch - * admin client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request * diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index cadbecb2e9a3f..2ced8b4762406 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -279,9 +279,10 @@ export class LegacyService implements CoreService { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, elasticsearch: { - adminClient: setupDeps.core.elasticsearch.adminClient, - dataClient: setupDeps.core.elasticsearch.dataClient, - createClient: setupDeps.core.elasticsearch.createClient, + legacy: { + client: setupDeps.core.elasticsearch.legacy.client, + createClient: setupDeps.core.elasticsearch.legacy.createClient, + }, }, http: { createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index f0fd471abb9be..b6e9ffef6f3f1 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -101,6 +101,7 @@ function pluginInitializerContextMock(config: T = {} as T) { } type CoreSetupMockType = MockedKeys & { + elasticsearch: ReturnType; getStartServices: jest.MockedFunction>; }; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index ab18a9cbbc062..7afb607192cae 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -147,9 +147,7 @@ export function createPluginSetupContext( createContextContainer: deps.context.createContextContainer, }, elasticsearch: { - adminClient: deps.elasticsearch.adminClient, - dataClient: deps.elasticsearch.dataClient, - createClient: deps.elasticsearch.createClient, + legacy: deps.elasticsearch.legacy, }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index bff392e8761eb..9fba2728003d2 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -67,6 +67,13 @@ describe('SavedObjectsService', () => { }; }; + const createStartDeps = (pluginsInitialized: boolean = true) => { + return { + pluginsInitialized, + elasticsearch: elasticsearchServiceMock.createStart(), + }; + }; + afterEach(() => { jest.clearAllMocks(); }); @@ -83,7 +90,7 @@ describe('SavedObjectsService', () => { setup.setClientFactoryProvider(factoryProvider); - await soService.start({}); + await soService.start(createStartDeps()); expect(clientProviderInstanceMock.setClientFactory).toHaveBeenCalledWith(factory); }); @@ -117,7 +124,7 @@ describe('SavedObjectsService', () => { setup.addClientWrapper(1, 'A', wrapperA); setup.addClientWrapper(2, 'B', wrapperB); - await soService.start({}); + await soService.start(createStartDeps()); expect(clientProviderInstanceMock.addClientWrapperFactory).toHaveBeenCalledTimes(2); expect(clientProviderInstanceMock.addClientWrapperFactory).toHaveBeenCalledWith( @@ -159,9 +166,10 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); + const coreStart = createStartDeps(); let i = 0; - coreSetup.elasticsearch.adminClient.callAsInternalUser = jest + coreStart.elasticsearch.legacy.client.callAsInternalUser = jest .fn() .mockImplementation(() => i++ <= 2 @@ -170,7 +178,7 @@ describe('SavedObjectsService', () => { ); await soService.setup(coreSetup); - await soService.start({}, 1); + await soService.start(coreStart, 1); return expect(KibanaMigratorMock.mock.calls[0][0].callCluster()).resolves.toMatch('success'); }); @@ -180,7 +188,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - await soService.start({ pluginsInitialized: false }); + await soService.start(createStartDeps(false)); expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); @@ -188,7 +196,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: true }); const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - await soService.start({}); + await soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); @@ -206,7 +214,7 @@ describe('SavedObjectsService', () => { kibanaVersion: '8.0.0', }); await soService.setup(setupDeps); - soService.start({}); + soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< NodesVersionCompatibility @@ -228,7 +236,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); - const startContract = await soService.start({}); + const startContract = await soService.start(createStartDeps()); expect(startContract.migrator).toBe(migratorInstanceMock); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); @@ -237,7 +245,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: false }); const soService = new SavedObjectsService(coreContext); const setup = await soService.setup(createSetupDeps()); - await soService.start({}); + await soService.start(createStartDeps()); expect(() => { setup.setClientFactoryProvider(jest.fn()); @@ -268,7 +276,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: false }); const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - const { getTypeRegistry } = await soService.start({}); + const { getTypeRegistry } = await soService.start(createStartDeps()); expect(getTypeRegistry()).toBe(typeRegistryInstanceMock); }); @@ -280,18 +288,19 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createScopedRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createScopedRepository } = await soService.start(coreStart); const req = {} as KibanaRequest; createScopedRepository(req); - expect(coreSetup.elasticsearch.adminClient.asScoped).toHaveBeenCalledWith(req); + expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledWith(req); const [ { value: { callAsCurrentUser }, }, - ] = coreSetup.elasticsearch.adminClient.asScoped.mock.results; + ] = coreStart.elasticsearch.legacy.client.asScoped.mock.results; const [ [, , , callCluster, includedHiddenTypes], @@ -306,7 +315,8 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createScopedRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createScopedRepository } = await soService.start(coreStart); const req = {} as KibanaRequest; createScopedRepository(req, ['someHiddenType']); @@ -325,7 +335,8 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createInternalRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createInternalRepository } = await soService.start(coreStart); createInternalRepository(); @@ -333,8 +344,8 @@ describe('SavedObjectsService', () => { [, , , callCluster, includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; - expect(coreSetup.elasticsearch.adminClient.callAsInternalUser).toBe(callCluster); - expect(callCluster).toBe(coreSetup.elasticsearch.adminClient.callAsInternalUser); + expect(coreStart.elasticsearch.legacy.client.callAsInternalUser).toBe(callCluster); + expect(callCluster).toBe(coreStart.elasticsearch.legacy.client.callAsInternalUser); expect(includedHiddenTypes).toEqual([]); }); @@ -343,7 +354,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createInternalRepository } = await soService.start({}); + const { createInternalRepository } = await soService.start(createStartDeps()); createInternalRepository(['someHiddenType']); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index a822a92acb91a..48b1e12fc187e 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -29,7 +29,12 @@ import { import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; import { LegacyServiceDiscoverPlugins } from '../legacy'; -import { InternalElasticsearchServiceSetup, APICaller } from '../elasticsearch'; +import { + APICaller, + ElasticsearchServiceStart, + IClusterClient, + InternalElasticsearchServiceSetup, +} from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { @@ -278,8 +283,8 @@ interface WrappedClientFactoryWrapper { } /** @internal */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsStartDeps { + elasticsearch: ElasticsearchServiceStart; pluginsInitialized?: boolean; } @@ -365,7 +370,7 @@ export class SavedObjectsService } public async start( - { pluginsInitialized = true }: SavedObjectsStartDeps, + { elasticsearch, pluginsInitialized = true }: SavedObjectsStartDeps, migrationsRetryDelay?: number ): Promise { if (!this.setupDeps || !this.config) { @@ -378,8 +383,14 @@ export class SavedObjectsService .atPath('kibana') .pipe(first()) .toPromise(); - const adminClient = this.setupDeps!.elasticsearch.adminClient; - const migrator = this.createMigrator(kibanaConfig, this.config.migration, migrationsRetryDelay); + const client = elasticsearch.legacy.client; + + const migrator = this.createMigrator( + kibanaConfig, + this.config.migration, + client, + migrationsRetryDelay + ); this.migrator$.next(migrator); @@ -435,9 +446,9 @@ export class SavedObjectsService const repositoryFactory: SavedObjectsRepositoryFactory = { createInternalRepository: (includedHiddenTypes?: string[]) => - createRepository(adminClient.callAsInternalUser, includedHiddenTypes), + createRepository(client.callAsInternalUser, includedHiddenTypes), createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => - createRepository(adminClient.asScoped(req).callAsCurrentUser, includedHiddenTypes), + createRepository(client.asScoped(req).callAsCurrentUser, includedHiddenTypes), }; const clientProvider = new SavedObjectsClientProvider({ @@ -473,10 +484,9 @@ export class SavedObjectsService private createMigrator( kibanaConfig: KibanaConfigType, savedObjectsConfig: SavedObjectsMigrationConfigType, + esClient: IClusterClient, migrationsRetryDelay?: number ): KibanaMigrator { - const adminClient = this.setupDeps!.elasticsearch.adminClient; - return new KibanaMigrator({ typeRegistry: this.typeRegistry, logger: this.logger, @@ -485,7 +495,7 @@ export class SavedObjectsService savedObjectValidations: this.validations, kibanaConfig, callCluster: migrationsRetryCallCluster( - adminClient.callAsInternalUser, + esClient.callAsInternalUser, this.logger, migrationsRetryDelay ), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 858458bfe40de..eef071e9488bf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -823,16 +823,15 @@ export class ElasticsearchErrorHelpers { // @public (undocumented) export interface ElasticsearchServiceSetup { // @deprecated (undocumented) - readonly adminClient: IClusterClient; - // @deprecated (undocumented) - readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; - // @deprecated (undocumented) - readonly dataClient: IClusterClient; + legacy: { + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; + readonly client: IClusterClient; + }; } // @public (undocumented) export interface ElasticsearchServiceStart { - // (undocumented) + // @deprecated (undocumented) legacy: { readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; readonly client: IClusterClient; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index b33528e2a3931..ef12379c199e8 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -195,12 +195,13 @@ export class Server { public async start() { this.log.debug('starting server'); + const elasticsearchStart = await this.elasticsearch.start(); const savedObjectsStart = await this.savedObjects.start({ + elasticsearch: elasticsearchStart, pluginsInitialized: this.pluginsInitialized, }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); - const elasticsearchStart = await this.elasticsearch.start(); this.coreStart = { capabilities: capabilitiesStart, diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index a7d6810ac6158..eb502e97fb77c 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -34,9 +34,9 @@ export default function (kibana) { // All methods that ES plugin exposes are synchronous so we should get the first // value from all observables here to be able to synchronously return and create // cluster clients afterwards. - const { adminClient, dataClient } = server.newPlatform.setup.core.elasticsearch; - const adminCluster = new Cluster(adminClient); - const dataCluster = new Cluster(dataClient); + const { client } = server.newPlatform.setup.core.elasticsearch.legacy; + const adminCluster = new Cluster(client); + const dataCluster = new Cluster(client); const esConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$ .pipe(first()) @@ -72,7 +72,7 @@ export default function (kibana) { } const cluster = new Cluster( - server.newPlatform.setup.core.elasticsearch.createClient(name, clientConfig) + server.newPlatform.setup.core.elasticsearch.legacy.createClient(name, clientConfig) ); clusters.set(name, cluster); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 5b40cc4b5aa35..63e4a632ab5e0 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -129,13 +129,6 @@ describe('Saved Objects Mixin', () => { waitUntilReady: jest.fn(), }, }, - newPlatform: { - __internals: { - elasticsearch: { - adminClient: { callAsInternalUser: mockCallCluster }, - }, - }, - }, }; const coreStart = coreMock.createStart(); diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 8ae63682413e5..e555c40d25592 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -82,7 +82,7 @@ export class TelemetryPlugin implements Plugin { const config$ = this.config$; const isDev = this.isDev; - registerCollection(telemetryCollectionManager, elasticsearch.dataClient); + registerCollection(telemetryCollectionManager, elasticsearch.legacy.client); const router = http.createRouter(); registerRoutes({ diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts index f6ae8edb9d5cb..dd7b37d989acb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts @@ -50,8 +50,10 @@ describe('CSV Execute Job', function () { let cancellationToken: any; const mockElasticsearch = { - dataClient: { - asScoped: () => clusterStub, + legacy: { + client: { + asScoped: () => clusterStub, + }, }, }; const mockUiSettingsClient = { diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 7d95c45d5d233..a6b2b0d0561d0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -81,7 +81,7 @@ export const executeJobFactory: ExecuteJobFactory callAsCurrentUser(endpoint, clientParams, options); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 51e0ddad53355..848623ede5b2f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -149,7 +149,7 @@ export async function generateCsvSearch( const config = reporting.getConfig(); const elasticsearch = await reporting.getElasticsearchService(); - const { callAsCurrentUser } = elasticsearch.dataClient.asScoped(req); + const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts index 72695545e3b5f..7e816e11f1953 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts @@ -59,8 +59,10 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, }, }; const mockGetElasticsearch = jest.fn(); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts index b081521fef8dd..fdf9c24730638 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts @@ -57,8 +57,10 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, }, }; const mockGetElasticsearch = jest.fn(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 2cac4bd654487..d993a17c0b314 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -52,7 +52,7 @@ export async function createQueueFactory( interval: queueIndexInterval, timeout: queueTimeout, dateSeparator: '.', - client: elasticsearch.dataClient, + client: elasticsearch.legacy.client, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 5153fd0f4e5b8..06c4a7714099e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -47,7 +47,7 @@ export function jobsQueryFactory( elasticsearch: ElasticsearchServiceSetup ) { const index = config.get('index'); - const { callAsInternalUser } = elasticsearch.adminClient; + const { callAsInternalUser } = elasticsearch.legacy.client; function execQuery(queryType: string, body: QueryBody) { const defaultBody: Record = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 2551fd48b91f3..f358021560cff 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -12,14 +12,16 @@ const ONE_HUNDRED_MEGABYTES = 104857600; describe('Reporting: Validate Max Content Length', () => { const elasticsearch = { - dataClient: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', + legacy: { + client: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, }, - }, - }), + }), + }, }, }; @@ -34,14 +36,16 @@ describe('Reporting: Validate Max Content Length', () => { it('should log warning messages when reporting has a higher max-size than elasticsearch', async () => { const config = { get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES) }; const elasticsearch = { - dataClient: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', + legacy: { + client: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, }, - }, - }), + }), + }, }, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index f6acf72612e01..6d34937d9bd75 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -18,7 +18,7 @@ export async function validateMaxContentLength( elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) { - const { callAsInternalUser } = elasticsearch.dataClient; + const { callAsInternalUser } = elasticsearch.legacy.client; const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { includeDefaults: true, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts index fdde3253cf28e..87ac71e250d0c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -51,7 +51,9 @@ describe('POST /api/reporting/generate', () => { ({ server, httpSetup } = await setupServer()); const mockDeps = ({ elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, + legacy: { + client: { callAsInternalUser: jest.fn() }, + }, }, security: { authc: { diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts index 73f3c660141c1..0911f48f82ca4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts @@ -43,7 +43,7 @@ describe('GET /api/reporting/jobs/download', () => { ({ server, httpSetup } = await setupServer()); core = await createMockReportingCore(config, ({ elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, + legacy: { client: { callAsInternalUser: jest.fn() } }, }, security: { authc: { @@ -89,7 +89,7 @@ describe('GET /api/reporting/jobs/download', () => { it('fails on malformed download IDs', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; registerJobInfoRoutes(core); @@ -158,7 +158,7 @@ describe('GET /api/reporting/jobs/download', () => { it('returns 404 if job not found', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; @@ -171,7 +171,7 @@ describe('GET /api/reporting/jobs/download', () => { it('returns a 401 if not a valid job type', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest .fn() .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), @@ -185,7 +185,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a job is incomplete', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest .fn() .mockReturnValue( @@ -205,7 +205,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a job fails', async () => { // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue( Promise.resolve( getHits({ @@ -248,7 +248,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a known job-type is complete', async () => { const hits = getCompleteHits(); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); @@ -264,7 +264,7 @@ describe('GET /api/reporting/jobs/download', () => { it('succeeds when security is not there or disabled', async () => { const hits = getCompleteHits(); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; @@ -288,7 +288,7 @@ describe('GET /api/reporting/jobs/download', () => { outputContent: 'test', }); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); @@ -308,7 +308,7 @@ describe('GET /api/reporting/jobs/download', () => { outputContentType: 'application/pdf', }); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); @@ -329,7 +329,7 @@ describe('GET /api/reporting/jobs/download', () => { outputContentType: 'application/html', }); // @ts-ignore - core.pluginSetupDeps.elasticsearch.adminClient = { + core.pluginSetupDeps.elasticsearch.legacy.client = { callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), }; registerJobInfoRoutes(core); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 33f70c549914d..e1aa4a1b32517 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -107,7 +107,7 @@ export class LicensingPlugin implements Plugin ): ReturnType { const [coreStart] = await core.getStartServices(); - const client = coreStart.elasticsearch.legacy.client; - return await client.asScoped(request).callAsCurrentUser(...args); + const _client = coreStart.elasticsearch.legacy.client; + return await _client.asScoped(request).callAsCurrentUser(...args); }, callAsInternalUser, }; @@ -124,7 +124,7 @@ export class LicensingPlugin implements Plugin { + this._cluster = elasticsearch.legacy.createClient('monitoring-direct', config.elasticsearch); + elasticsearch.legacy.client.callAsInternalUser('info').then((data) => { this._productionClusterUuid = get(data, 'cluster_uuid'); }); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index a45e80ac71d65..14bef307b2f85 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -131,7 +131,7 @@ export class Plugin { this.legacyShimDependencies = { router: core.http.createRouter(), instanceUuid: core.uuid.getInstanceUuid(), - esDataClient: core.elasticsearch.dataClient, + esDataClient: core.elasticsearch.legacy.client, kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( KIBANA_STATS_TYPE_MONITORING ), @@ -142,7 +142,7 @@ export class Plugin { const cluster = (this.cluster = instantiateClient( config.ui.elasticsearch, this.log, - core.elasticsearch.createClient + core.elasticsearch.legacy.createClient )); // Start our license service which will ensure diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index cbfbbfd2d61f3..d28e95834ca0b 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -28,7 +28,7 @@ describe('ADD remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, payload }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index b9328cb61e967..d1e3cf89e94d9 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -30,7 +30,7 @@ describe('DELETE remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, params }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index f0444162e80b9..24e469c9ec9b2 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -29,7 +29,7 @@ describe('GET remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 7f8acb2b018d9..9669c98e1349e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -37,7 +37,7 @@ describe('UPDATE remote clusters', () => { }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index fc49bdd9bc0c3..3e30ff9447f3e 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -39,9 +39,7 @@ describe('Security Plugin', () => { mockCoreSetup.http.isTlsEnabled = true; mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); - mockCoreSetup.elasticsearch.createClient.mockReturnValue( - (mockClusterClient as unknown) as jest.Mocked - ); + mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; }); @@ -114,8 +112,8 @@ describe('Security Plugin', () => { it('properly creates cluster client instance', async () => { await plugin.setup(mockCoreSetup, mockDependencies); - expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledTimes(1); - expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledWith('security', { + expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledTimes(1); + expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledWith('security', { plugins: [elasticsearchClientPlugin], }); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index cdfc6f0ae542f..bdda0be9b15a7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -109,7 +109,7 @@ export class Plugin { .pipe(first()) .toPromise(); - this.clusterClient = core.elasticsearch.createClient('security', { + this.clusterClient = core.elasticsearch.legacy.createClient('security', { plugins: [elasticsearchClientPlugin], }); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index b0afba8852495..3f01d7423eded 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -18,7 +18,7 @@ export class TelemetryCollectionXpackPlugin implements Plugin { public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { telemetryCollectionManager.setCollection({ - esCluster: core.elasticsearch.dataClient, + esCluster: core.elasticsearch.legacy.client, title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index a921dac7d43a1..3b6befb3fe807 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server'; @@ -13,7 +13,7 @@ export function defineActionTypes( core: CoreSetup, { actions }: Pick ) { - const clusterClient = core.elasticsearch.adminClient; + const clusterClient = core.elasticsearch.legacy.client; // Action types const noopActionType: ActionType = { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index ff3a3e48d5b1a..bfabbb8169391 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { times } from 'lodash'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; @@ -14,7 +14,7 @@ export function defineAlertTypes( core: CoreSetup, { alerting }: Pick ) { - const clusterClient = core.elasticsearch.adminClient; + const clusterClient = core.elasticsearch.legacy.client; const alwaysFiringAlertType: AlertType = { id: 'test.always-firing', name: 'Test: Always Firing', diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index e4c4d13ee4a41..f35d6baac8f5a 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -11,7 +11,7 @@ import { IKibanaResponse, IRouter, CoreSetup, -} from 'kibana/server'; +} from 'src/core/server'; import { EventEmitter } from 'events'; import { TaskManagerStartContract } from '../../../../../plugins/task_manager/server'; @@ -39,7 +39,7 @@ export function initRoutes( taskTestingEvents: EventEmitter ) { async function ensureIndexIsRefreshed() { - return await core.elasticsearch.adminClient.callAsInternalUser('indices.refresh', { + return await core.elasticsearch.legacy.client.callAsInternalUser('indices.refresh', { index: '.kibana_task_manager', }); } diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts index ae756bb56b921..3ea669ae9d404 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; import { EventEmitter } from 'events'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; @@ -64,7 +64,7 @@ export class SampleTaskManagerFixturePlugin } } - await core.elasticsearch.adminClient.callAsInternalUser('index', { + await core.elasticsearch.legacy.client.callAsInternalUser('index', { index: '.kibana_task_manager_test_result', body: { type: 'task', From cf2aebf67a4280b94ec25371ba902314e580ec28 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 1 Jun 2020 11:02:46 -0400 Subject: [PATCH 25/69] [Ingest Manager] Optimize installation of integration (#67708) * call getArchiveInfo once first, pass paths to template * pass paths to installPreBuiltTemplates * pass paths to installILMPolicy * pass paths to ingest pipeline creation * use correct package key for cache * pass paths to kibana assets * cache other installed packages * create function for ensuring packages are cached * remove unused imports Co-authored-by: Elastic Machine --- .../services/epm/elasticsearch/ilm/install.ts | 14 ++------ .../elasticsearch/ingest_pipeline/install.ts | 22 +++++++------ .../epm/elasticsearch/template/install.ts | 32 ++++++------------- .../epm/kibana/index_pattern/install.ts | 8 +++++ .../server/services/epm/packages/assets.ts | 6 ++-- .../server/services/epm/packages/install.ts | 28 ++++++++-------- .../server/services/epm/registry/cache.ts | 1 + .../server/services/epm/registry/index.ts | 11 +++++-- 8 files changed, 60 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts index 1d06bf23a8c0f..9590167657d98 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ilm/install.ts @@ -7,16 +7,8 @@ import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; import * as Registry from '../../registry'; -export async function installILMPolicy( - pkgName: string, - pkgVersion: string, - callCluster: CallESAsCurrentUser -) { - const ilmPaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isILMPolicy(entry) - ); +export async function installILMPolicy(paths: string[], callCluster: CallESAsCurrentUser) { + const ilmPaths = paths.filter((path) => isILMPolicy(path)); if (!ilmPaths.length) return; await Promise.all( ilmPaths.map(async (path) => { @@ -36,7 +28,7 @@ export async function installILMPolicy( }) ); } -const isILMPolicy = ({ path }: Registry.ArchiveEntry) => { +const isILMPolicy = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.ilmPolicy; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index bdf6ecfcdb9aa..11543fe73886f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -22,9 +22,11 @@ interface RewriteSubstitution { export const installPipelines = async ( registryPackage: RegistryPackage, + paths: string[], callCluster: CallESAsCurrentUser ) => { const datasets = registryPackage.datasets; + const pipelinePaths = paths.filter((path) => isPipeline(path)); if (datasets) { const pipelines = datasets.reduce>>((acc, dataset) => { if (dataset.ingest_pipeline) { @@ -32,7 +34,7 @@ export const installPipelines = async ( installPipelinesForDataset({ dataset, callCluster, - pkgName: registryPackage.name, + paths: pipelinePaths, pkgVersion: registryPackage.version, }) ); @@ -67,20 +69,16 @@ export function rewriteIngestPipeline( export async function installPipelinesForDataset({ callCluster, - pkgName, pkgVersion, + paths, dataset, }: { callCluster: CallESAsCurrentUser; - pkgName: string; pkgVersion: string; + paths: string[]; dataset: Dataset; }): Promise { - const pipelinePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isDatasetPipeline(entry, dataset.path) - ); + const pipelinePaths = paths.filter((path) => isDatasetPipeline(path, dataset.path)); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -152,8 +150,8 @@ async function installPipeline({ } const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/'); -const isDatasetPipeline = ({ path }: Registry.ArchiveEntry, datasetName: string) => { - // TODO: better way to get particular assets + +const isDatasetPipeline = (path: string, datasetName: string) => { const pathParts = Registry.pathParts(path); return ( !isDirectory({ path }) && @@ -162,6 +160,10 @@ const isDatasetPipeline = ({ path }: Registry.ArchiveEntry, datasetName: string) datasetName === pathParts.dataset ); }; +const isPipeline = (path: string) => { + const pathParts = Registry.pathParts(path); + return pathParts.type === ElasticsearchAssetType.ingestPipeline; +}; // XXX: assumes path/to/file.ext -- 0..n '/' and exactly one '.' const getNameAndExtension = ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index c600c8ba3efb8..9d0b6b5d078ad 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -16,13 +16,14 @@ export const installTemplates = async ( registryPackage: RegistryPackage, callCluster: CallESAsCurrentUser, pkgName: string, - pkgVersion: string + pkgVersion: string, + paths: string[] ): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates - await installPreBuiltComponentTemplates(pkgName, pkgVersion, callCluster); - await installPreBuiltTemplates(pkgName, pkgVersion, callCluster); + await installPreBuiltComponentTemplates(paths, callCluster); + await installPreBuiltTemplates(paths, callCluster); // build templates per dataset from yml files const datasets = registryPackage.datasets; @@ -44,16 +45,8 @@ export const installTemplates = async ( return []; }; -const installPreBuiltTemplates = async ( - pkgName: string, - pkgVersion: string, - callCluster: CallESAsCurrentUser -) => { - const templatePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isTemplate(entry) - ); +const installPreBuiltTemplates = async (paths: string[], callCluster: CallESAsCurrentUser) => { + const templatePaths = paths.filter((path) => isTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); @@ -95,15 +88,10 @@ const installPreBuiltTemplates = async ( }; const installPreBuiltComponentTemplates = async ( - pkgName: string, - pkgVersion: string, + paths: string[], callCluster: CallESAsCurrentUser ) => { - const templatePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isComponentTemplate(entry) - ); + const templatePaths = paths.filter((path) => isComponentTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); @@ -134,12 +122,12 @@ const installPreBuiltComponentTemplates = async ( } }; -const isTemplate = ({ path }: Registry.ArchiveEntry) => { +const isTemplate = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.indexTemplate; }; -const isComponentTemplate = ({ path }: Registry.ArchiveEntry) => { +const isComponentTemplate = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.componentTemplate; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index f321e2d614a04..0f7b1d6cab178 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -86,6 +86,14 @@ export async function installIndexPatterns( savedObjectsClient, InstallationStatus.installed ); + + // TODO: move to install package + // cache all installed packages if they don't exist + const packagePromises = installedPackages.map((pkg) => + Registry.ensureCachedArchiveInfo(pkg.pkgName, pkg.pkgVersion) + ); + await Promise.all(packagePromises); + if (pkgName && pkgVersion) { // add this package to the array if it doesn't already exist const foundPkg = installedPackages.find((pkg) => pkg.pkgName === pkgName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index c6f7a1f6b97aa..37fcf0db67131 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { RegistryPackage } from '../../../types'; import * as Registry from '../registry'; -import { cacheHas } from '../registry/cache'; +import { ensureCachedArchiveInfo } from '../registry'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml` @@ -57,8 +57,8 @@ export async function getAssetsData( datasetName?: string ): Promise { // TODO: Needs to be called to fill the cache but should not be required - const pkgkey = packageInfo.name + '-' + packageInfo.version; - if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(packageInfo.name, packageInfo.version); + + await ensureCachedArchiveInfo(packageInfo.name, packageInfo.version); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index dddb21bc4e075..7c0d5d571f6a5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -90,7 +90,7 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); - + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); // see if some version of this package is already installed // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets @@ -119,15 +119,16 @@ export async function installPackage(options: { savedObjectsClient, pkgName, pkgVersion, + paths, }), - installPipelines(registryPackageInfo, callCluster), + installPipelines(registryPackageInfo, paths, callCluster), // index patterns and ilm policies are not currently associated with a particular package // so we do not save them in the package saved object state. installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), // currenly only the base package has an ILM policy // at some point ILM policies can be installed/modified // per dataset and we should then save them - installILMPolicy(pkgName, pkgVersion, callCluster), + installILMPolicy(paths, callCluster), ]); // install or update the templates @@ -135,7 +136,8 @@ export async function installPackage(options: { registryPackageInfo, callCluster, pkgName, - pkgVersion + pkgVersion, + paths ); const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); @@ -186,13 +188,14 @@ export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; + paths: string[]; }) { - const { savedObjectsClient, pkgName, pkgVersion } = options; + const { savedObjectsClient, paths } = options; // Only install Kibana assets during package installation. const kibanaAssetTypes = Object.values(KibanaAssetType); const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, pkgName, pkgVersion, assetType }) + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) ); // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] @@ -237,19 +240,16 @@ export async function saveInstallationReferences(options: { async function installKibanaSavedObjects({ savedObjectsClient, - pkgName, - pkgVersion, assetType, + paths, }: { savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; assetType: KibanaAssetType; + paths: string[]; }) { - const isSameType = ({ path }: Registry.ArchiveEntry) => - assetType === Registry.pathParts(path).type; - const paths = await Registry.getArchiveInfo(pkgName, pkgVersion, isSameType); - const toBeSavedObjects = await Promise.all(paths.map(getObject)); + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); if (toBeSavedObjects.length === 0) { return []; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts index 17d52bc745a55..d2a14fcf04dff 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts @@ -8,3 +8,4 @@ const cache: Map = new Map(); export const cacheGet = (key: string) => cache.get(key); export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); export const cacheHas = (key: string) => cache.has(key); +export const getCacheKey = (key: string) => key + '.tar.gz'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 8e9b920875617..0393cabca8ba2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -16,7 +16,7 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { cacheGet, cacheSet } from './cache'; +import { cacheGet, cacheSet, getCacheKey, cacheHas } from './cache'; import { ArchiveEntry, untarBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; @@ -135,7 +135,7 @@ async function extract( async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { // assume .tar.gz for now. add support for .zip if/when we need it - const key = `${pkgName}-${pkgVersion}.tar.gz`; + const key = getCacheKey(`${pkgName}-${pkgVersion}`); let buffer = cacheGet(key); if (!buffer) { buffer = await fetchArchiveBuffer(pkgName, pkgVersion); @@ -149,6 +149,13 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro } } +export async function ensureCachedArchiveInfo(name: string, version: string) { + const pkgkey = getCacheKey(`${name}-${version}`); + if (!cacheHas(pkgkey)) { + await getArchiveInfo(name, version); + } +} + async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); const registryUrl = getRegistryUrl(); From f31330a01b21cda1a35a83a6dbf4998264817fc6 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Mon, 1 Jun 2020 10:53:33 -0500 Subject: [PATCH 26/69] [ML] Add ability to delete target index & index pattern when deleting DFA job (#66934) Co-authored-by: Elastic Machine --- .../ml/common/types/data_frame_analytics.ts | 11 + .../analytics_list/action_delete.test.tsx | 51 +++- .../analytics_list/action_delete.tsx | 150 +++++++++++- .../analytics_service/delete_analytics.ts | 131 ++++++++++- .../services/analytics_service/index.ts | 3 +- .../ml_api_service/data_frame_analytics.ts | 19 ++ .../services/ml_api_service/jobs.ts | 1 - .../ml/public/application/util/error_utils.ts | 32 +++ .../data_frame_analytics/index_patterns.ts | 32 +++ .../ml/server/routes/data_frame_analytics.ts | 112 ++++++++- .../routes/schemas/data_analytics_schema.ts | 8 + .../apis/ml/data_frame_analytics/delete.ts | 218 ++++++++++++++++++ .../apis/ml/data_frame_analytics/index.ts | 1 + x-pack/test/functional/services/ml/api.ts | 53 ++++- .../functional/services/ml/test_resources.ts | 17 +- 15 files changed, 797 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/data_frame_analytics.ts create mode 100644 x-pack/plugins/ml/public/application/util/error_utils.ts create mode 100644 x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts new file mode 100644 index 0000000000000..5ba7f9c191a7f --- /dev/null +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; +export interface DeleteDataFrameAnalyticsWithIndexStatus { + success: boolean; + error?: CustomHttpResponseOptions; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 2ef1515726d1b..33217f127f998 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -5,20 +5,41 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; - +import { fireEvent, render } from '@testing-library/react'; import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; - -import { DeleteAction } from './action_delete'; - import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; +import { DeleteAction } from './action_delete'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + coreMock as mockCoreServices, + i18nServiceMock, +} from '../../../../../../../../../../src/core/public/mocks'; jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); +jest.mock('../../../../../../application/util/dependency_cache', () => ({ + getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), +})); + +jest.mock('../../../../../contexts/kibana', () => ({ + useMlKibana: () => ({ + services: mockCoreServices.createStart(), + }), +})); +export const MockI18nService = i18nServiceMock.create(); +export const I18nServiceConstructor = jest.fn().mockImplementation(() => MockI18nService); +jest.doMock('@kbn/i18n', () => ({ + I18nService: I18nServiceConstructor, +})); + describe('DeleteAction', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { const { getByTestId } = render(); expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); @@ -46,4 +67,24 @@ describe('DeleteAction', () => { expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); }); + + describe('When delete model is open', () => { + test('should allow to delete target index by default.', () => { + const mock = jest.spyOn(CheckPrivilige, 'checkPermission'); + mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics'); + const { getByTestId, queryByTestId } = render( + + + + ); + const deleteButton = getByTestId('mlAnalyticsJobDeleteButton'); + fireEvent.click(deleteButton); + expect(getByTestId('mlAnalyticsJobDeleteModal')).toBeInTheDocument(); + expect(getByTestId('mlAnalyticsJobDeleteIndexSwitch')).toBeInTheDocument(); + const mlAnalyticsJobDeleteIndexSwitch = getByTestId('mlAnalyticsJobDeleteIndexSwitch'); + expect(mlAnalyticsJobDeleteIndexSwitch).toHaveAttribute('aria-checked', 'true'); + expect(queryByTestId('mlAnalyticsJobDeleteIndexPatternSwitch')).toBeNull(); + mock.mockRestore(); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 2923938ae68ac..2d433f6b18484 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -4,24 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState } from 'react'; +import React, { Fragment, FC, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask, EuiToolTip, + EuiSwitch, + EuiFlexGroup, + EuiFlexItem, EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; - -import { deleteAnalytics } from '../../services/analytics_service'; - +import { IIndexPattern } from 'src/plugins/data/common'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + deleteAnalytics, + deleteAnalyticsAndDestIndex, + canDeleteIndex, +} from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, } from '../../../../../capabilities/check_capabilities'; - +import { useMlKibana } from '../../../../../contexts/kibana'; import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; +import { extractErrorMessage } from '../../../../../util/error_utils'; interface DeleteActionProps { item: DataFrameAnalyticsListRow; @@ -29,17 +37,99 @@ interface DeleteActionProps { export const DeleteAction: FC = ({ item }) => { const disabled = isDataFrameAnalyticsRunning(item.stats.state); - const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); const [isModalVisible, setModalVisible] = useState(false); + const [deleteTargetIndex, setDeleteTargetIndex] = useState(true); + const [deleteIndexPattern, setDeleteIndexPattern] = useState(true); + const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); + const [indexPatternExists, setIndexPatternExists] = useState(false); + + const { savedObjects, notifications } = useMlKibana().services; + const savedObjectsClient = savedObjects.client; + + const indexName = item.config.dest.index; + + const checkIndexPatternExists = async () => { + try { + const response = await savedObjectsClient.find({ + type: 'index-pattern', + perPage: 10, + search: `"${indexName}"`, + searchFields: ['title'], + fields: ['title'], + }); + const ip = response.savedObjects.find( + (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() + ); + if (ip !== undefined) { + setIndexPatternExists(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if index pattern {indexPattern} exists: {error}', + values: { indexPattern: indexName, error }, + } + ) + ); + } + }; + const checkUserIndexPermission = () => { + try { + const userCanDelete = canDeleteIndex(indexName); + if (userCanDelete) { + setUserCanDeleteIndex(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if user can delete {destinationIndex}: {error}', + values: { destinationIndex: indexName, error }, + } + ) + ); + } + }; + + useEffect(() => { + // Check if an index pattern exists corresponding to current DFA job + // if pattern does exist, show it to user + checkIndexPatternExists(); + + // Check if an user has permission to delete the index & index pattern + checkUserIndexPermission(); + }, []); const closeModal = () => setModalVisible(false); const deleteAndCloseModal = () => { setModalVisible(false); - deleteAnalytics(item); + + if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) { + deleteAnalyticsAndDestIndex( + item, + deleteTargetIndex, + indexPatternExists && deleteIndexPattern + ); + } else { + deleteAnalytics(item); + } }; const openModal = () => setModalVisible(true); + const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex); + const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern); const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', { defaultMessage: 'Delete', @@ -84,8 +174,9 @@ export const DeleteAction: FC = ({ item }) => { {deleteButton} {isModalVisible && ( - + = ({ item }) => { buttonColor="danger" >

    - {i18n.translate('xpack.ml.dataframe.analyticsList.deleteModalBody', { - defaultMessage: `Are you sure you want to delete this analytics job? The analytics job's destination index and optional Kibana index pattern will not be deleted.`, - })} +

    + + + + {userCanDeleteIndex && ( + + )} + + + {userCanDeleteIndex && indexPatternExists && ( + + )} + +
    )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 7383f565bd673..26cefff0a3f59 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -3,17 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { i18n } from '@kbn/i18n'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; - import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; - import { isDataFrameAnalyticsFailed, DataFrameAnalyticsListRow, } from '../../components/analytics_list/common'; +import { extractErrorMessage } from '../../../../../util/error_utils'; export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { const toastNotifications = getToastNotifications(); @@ -24,18 +22,139 @@ export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { - defaultMessage: 'Request to delete data frame analytics {analyticsId} acknowledged.', + defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.', values: { analyticsId: d.config.id }, }) ); } catch (e) { + const error = extractErrorMessage(e); + toastNotifications.addDanger( i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { defaultMessage: - 'An error occurred deleting the data frame analytics {analyticsId}: {error}', - values: { analyticsId: d.config.id, error: JSON.stringify(e) }, + 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', + values: { analyticsId: d.config.id, error }, }) ); } refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); }; + +export const deleteAnalyticsAndDestIndex = async ( + d: DataFrameAnalyticsListRow, + deleteDestIndex: boolean, + deleteDestIndexPattern: boolean +) => { + const toastNotifications = getToastNotifications(); + const destinationIndex = Array.isArray(d.config.dest.index) + ? d.config.dest.index[0] + : d.config.dest.index; + try { + if (isDataFrameAnalyticsFailed(d.stats.state)) { + await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true); + } + const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex( + d.config.id, + deleteDestIndex, + deleteDestIndexPattern + ); + if (status.analyticsJobDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { + defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.', + values: { analyticsId: d.config.id }, + }) + ); + } + if (status.analyticsJobDeleted?.error) { + const error = extractErrorMessage(status.analyticsJobDeleted.error); + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { + defaultMessage: + 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', + values: { analyticsId: d.config.id, error }, + }) + ); + } + + if (status.destIndexDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage', { + defaultMessage: 'Request to delete destination index {destinationIndex} acknowledged.', + values: { destinationIndex }, + }) + ); + } + if (status.destIndexDeleted?.error) { + const error = extractErrorMessage(status.destIndexDeleted.error); + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', { + defaultMessage: + 'An error occurred deleting destination index {destinationIndex}: {error}', + values: { destinationIndex, error }, + }) + ); + } + + if (status.destIndexPatternDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage', + { + defaultMessage: 'Request to delete index pattern {destinationIndex} acknowledged.', + values: { destinationIndex }, + } + ) + ); + } + if (status.destIndexPatternDeleted?.error) { + const error = extractErrorMessage(status.destIndexPatternDeleted.error); + toastNotifications.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternErrorMessage', + { + defaultMessage: 'An error occurred deleting index pattern {destinationIndex}: {error}', + values: { destinationIndex, error }, + } + ) + ); + } + } catch (e) { + const error = extractErrorMessage(e); + + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { + defaultMessage: + 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', + values: { analyticsId: d.config.id, error }, + }) + ); + } + refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); +}; + +export const canDeleteIndex = async (indexName: string) => { + const toastNotifications = getToastNotifications(); + try { + const privilege = await ml.hasPrivileges({ + index: [ + { + names: [indexName], // uses wildcard + privileges: ['delete_index'], + }, + ], + }); + if (!privilege) { + return false; + } + return privilege.securityDisabled === true || privilege.has_all_requested === true; + } catch (e) { + const error = extractErrorMessage(e); + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage', { + defaultMessage: 'User does not have permission to delete index {indexName}: {error}', + values: { indexName, error }, + }) + ); + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts index 0d1a87e7c4c1f..68aa58e7e1f19 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - export { getAnalyticsFactory } from './get_analytics'; -export { deleteAnalytics } from './delete_analytics'; +export { deleteAnalytics, deleteAnalyticsAndDestIndex, canDeleteIndex } from './delete_analytics'; export { startAnalytics } from './start_analytics'; export { stopAnalytics } from './stop_analytics'; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 89950a659f609..7cdd5478e3983 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -10,6 +10,7 @@ import { basePath } from './index'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common'; import { DeepPartial } from '../../../../common/types/common'; +import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../../../common/types/data_frame_analytics'; export interface GetDataFrameAnalyticsStatsResponseOk { node_failures?: object; @@ -32,6 +33,13 @@ interface GetDataFrameAnalyticsResponse { data_frame_analytics: DataFrameAnalyticsConfig[]; } +interface DeleteDataFrameAnalyticsWithIndexResponse { + acknowledged: boolean; + analyticsJobDeleted: DeleteDataFrameAnalyticsWithIndexStatus; + destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus; + destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus; +} + export const dataFrameAnalytics = { getDataFrameAnalytics(analyticsId?: string) { const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; @@ -86,6 +94,17 @@ export const dataFrameAnalytics = { method: 'DELETE', }); }, + deleteDataFrameAnalyticsAndDestIndex( + analyticsId: string, + deleteDestIndex: boolean, + deleteDestIndexPattern: boolean + ) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}`, + query: { deleteDestIndex, deleteDestIndexPattern }, + method: 'DELETE', + }); + }, startDataFrameAnalytics(analyticsId: string) { return http({ path: `${basePath()}/data_frame/analytics/${analyticsId}/_start`, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 16e25067fd91e..e2569f6217b34 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -95,7 +95,6 @@ export const jobs = { body, }); }, - closeJobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); return http({ diff --git a/x-pack/plugins/ml/public/application/util/error_utils.ts b/x-pack/plugins/ml/public/application/util/error_utils.ts new file mode 100644 index 0000000000000..2ce8f4ffc583a --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/error_utils.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; + +export const extractErrorMessage = ( + error: CustomHttpResponseOptions | undefined | string +): string | undefined => { + if (typeof error === 'string') { + return error; + } + + if (error?.body) { + if (typeof error.body === 'string') { + return error.body; + } + if (typeof error.body === 'object' && 'message' in error.body) { + if (typeof error.body.message === 'string') { + return error.body.message; + } + // @ts-ignore + if (typeof (error.body.message?.msg === 'string')) { + // @ts-ignore + return error.body.message?.msg; + } + } + } + return undefined; +}; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts new file mode 100644 index 0000000000000..d1a4df768a6ae --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { IIndexPattern } from 'src/plugins/data/server'; + +export class IndexPatternHandler { + constructor(private savedObjectsClient: SavedObjectsClientContract) {} + // returns a id based on an index pattern name + async getIndexPatternId(indexName: string) { + const response = await this.savedObjectsClient.find({ + type: 'index-pattern', + perPage: 10, + search: `"${indexName}"`, + searchFields: ['title'], + fields: ['title'], + }); + + const ip = response.saved_objects.find( + (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() + ); + + return ip?.id; + } + + async deleteIndexPatternById(indexId: string) { + return await this.savedObjectsClient.delete('index-pattern', indexId); + } +} diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 894c4739ef96e..e2601c7ad6a2e 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; import { RouteInitialization } from '../types'; @@ -13,12 +14,48 @@ import { dataAnalyticsExplainSchema, analyticsIdSchema, stopsDataFrameAnalyticsJobQuerySchema, + deleteDataFrameAnalyticsJobSchema, } from './schemas/data_analytics_schema'; +import { IndexPatternHandler } from '../models/data_frame_analytics/index_patterns'; +import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics'; + +function getIndexPatternId(context: RequestHandlerContext, patternName: string) { + const iph = new IndexPatternHandler(context.core.savedObjects.client); + return iph.getIndexPatternId(patternName); +} + +function deleteDestIndexPatternById(context: RequestHandlerContext, indexPatternId: string) { + const iph = new IndexPatternHandler(context.core.savedObjects.client); + return iph.deleteIndexPatternById(indexPatternId); +} /** * Routes for the data frame analytics */ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitialization) { + async function userCanDeleteIndex( + context: RequestHandlerContext, + destinationIndex: string + ): Promise { + if (!mlLicense.isSecurityEnabled()) { + return true; + } + const privilege = await context.ml!.mlClient.callAsCurrentUser('ml.privilegeCheck', { + body: { + index: [ + { + names: [destinationIndex], // uses wildcard + privileges: ['delete_index'], + }, + ], + }, + }); + if (!privilege) { + return false; + } + return privilege.has_all_requested === true; + } + /** * @apiGroup DataFrameAnalytics * @@ -277,6 +314,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { params: analyticsIdSchema, + query: deleteDataFrameAnalyticsJobSchema, }, options: { tags: ['access:ml:canDeleteDataFrameAnalytics'], @@ -285,12 +323,78 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; - const results = await context.ml!.mlClient.callAsCurrentUser( - 'ml.deleteDataFrameAnalytics', - { + const { deleteDestIndex, deleteDestIndexPattern } = request.query; + let destinationIndex: string | undefined; + const analyticsJobDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { success: false }; + const destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { success: false }; + const destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { + success: false, + }; + + // Check if analyticsId is valid and get destination index + if (deleteDestIndex || deleteDestIndexPattern) { + try { + const dfa = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics', { + analyticsId, + }); + if (Array.isArray(dfa.data_frame_analytics) && dfa.data_frame_analytics.length > 0) { + destinationIndex = dfa.data_frame_analytics[0].dest.index; + } + } catch (e) { + return response.customError(wrapError(e)); + } + + // If user checks box to delete the destinationIndex associated with the job + if (destinationIndex && deleteDestIndex) { + // Verify if user has privilege to delete the destination index + const userCanDeleteDestIndex = await userCanDeleteIndex(context, destinationIndex); + // If user does have privilege to delete the index, then delete the index + if (userCanDeleteDestIndex) { + try { + await context.ml!.mlClient.callAsCurrentUser('indices.delete', { + index: destinationIndex, + }); + destIndexDeleted.success = true; + } catch (deleteIndexError) { + destIndexDeleted.error = wrapError(deleteIndexError); + } + } else { + return response.forbidden(); + } + } + + // Delete the index pattern if there's an index pattern that matches the name of dest index + if (destinationIndex && deleteDestIndexPattern) { + try { + const indexPatternId = await getIndexPatternId(context, destinationIndex); + if (indexPatternId) { + await deleteDestIndexPatternById(context, indexPatternId); + } + destIndexPatternDeleted.success = true; + } catch (deleteDestIndexPatternError) { + destIndexPatternDeleted.error = wrapError(deleteDestIndexPatternError); + } + } + } + // Grab the target index from the data frame analytics job id + // Delete the data frame analytics + + try { + await context.ml!.mlClient.callAsCurrentUser('ml.deleteDataFrameAnalytics', { analyticsId, + }); + analyticsJobDeleted.success = true; + } catch (deleteDFAError) { + analyticsJobDeleted.error = wrapError(deleteDFAError); + if (analyticsJobDeleted.error.statusCode === 404) { + return response.notFound(); } - ); + } + const results = { + analyticsJobDeleted, + destIndexDeleted, + destIndexPatternDeleted, + }; return response.ok({ body: results, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index f1d4947a7abc5..0b2469c103578 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -60,6 +60,14 @@ export const analyticsIdSchema = schema.object({ analyticsId: schema.string(), }); +export const deleteDataFrameAnalyticsJobSchema = schema.object({ + /** + * Analytics Destination Index + */ + deleteDestIndex: schema.maybe(schema.boolean()), + deleteDestIndexPattern: schema.maybe(schema.boolean()), +}); + export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({ force: schema.maybe(schema.boolean()), }); diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts new file mode 100644 index 0000000000000..23bff0d0c2855 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `bm_${Date.now()}`; + const generateDestinationIndex = (analyticsId: string) => `user-${analyticsId}`; + const commonJobConfig = { + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + analysis: { + classification: { + dependent_variable: 'y', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '350mb', + }; + + const testJobConfigs: Array> = [ + 'Test delete job only', + 'Test delete job and target index', + 'Test delete job and index pattern', + 'Test delete job, target index, and index pattern', + ].map((description, idx) => { + const analyticsId = `${jobId}_${idx + 1}`; + return { + id: analyticsId, + description, + dest: { + index: generateDestinationIndex(analyticsId), + results_field: 'ml', + }, + ...commonJobConfig, + }; + }); + + async function createJobs(mockJobConfigs: Array>) { + for (const jobConfig of mockJobConfigs) { + await ml.api.createDataFrameAnalyticsJob(jobConfig as DataFrameAnalyticsConfig); + } + } + + describe('DELETE data_frame/analytics', () => { + before(async () => { + await esArchiver.loadIfNeeded('ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await createJobs(testJobConfigs); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + describe('DeleteDataFrameAnalytics', () => { + it('should delete analytics jobs by id', async () => { + const analyticsId = `${jobId}_1`; + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + }); + + it('should not allow to retrieve analytics jobs for unauthorized user', async () => { + const analyticsId = `${jobId}_2`; + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForDataFrameAnalyticsJobToExist(analyticsId); + }); + + it('should not allow to retrieve analytics jobs for the user with only view permission', async () => { + const analyticsId = `${jobId}_2`; + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForDataFrameAnalyticsJobToExist(analyticsId); + }); + + it('should show 404 error if job does not exist or has already been deleted', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${jobId}_invalid`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + + describe('with deleteDestIndex setting', function () { + const analyticsId = `${jobId}_2`; + const destinationIndex = generateDestinationIndex(analyticsId); + + before(async () => { + await ml.api.createIndices(destinationIndex); + await ml.api.assertIndicesExist(destinationIndex); + }); + + after(async () => { + await ml.api.deleteIndices(destinationIndex); + }); + + it('should delete job and destination index by id', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .query({ deleteDestIndex: true }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + expect(body.destIndexDeleted.success).to.eql(true); + expect(body.destIndexPatternDeleted.success).to.eql(false); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + await ml.api.assertIndicesNotToExist(destinationIndex); + }); + }); + + describe('with deleteDestIndexPattern setting', function () { + const analyticsId = `${jobId}_3`; + const destinationIndex = generateDestinationIndex(analyticsId); + + before(async () => { + // Mimic real job by creating index pattern after job is created + await ml.testResources.createIndexPatternIfNeeded(destinationIndex); + }); + + after(async () => { + await ml.testResources.deleteIndexPattern(destinationIndex); + }); + + it('should delete job and index pattern by id', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .query({ deleteDestIndexPattern: true }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + expect(body.destIndexDeleted.success).to.eql(false); + expect(body.destIndexPatternDeleted.success).to.eql(true); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + await ml.testResources.assertIndexPatternNotExist(destinationIndex); + }); + }); + + describe('with deleteDestIndex & deleteDestIndexPattern setting', function () { + const analyticsId = `${jobId}_4`; + const destinationIndex = generateDestinationIndex(analyticsId); + + before(async () => { + // Mimic real job by creating target index & index pattern after DFA job is created + await ml.api.createIndices(destinationIndex); + await ml.api.assertIndicesExist(destinationIndex); + await ml.testResources.createIndexPatternIfNeeded(destinationIndex); + }); + + after(async () => { + await ml.api.deleteIndices(destinationIndex); + await ml.testResources.deleteIndexPattern(destinationIndex); + }); + + it('deletes job, target index, and index pattern by id', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .query({ deleteDestIndex: true, deleteDestIndexPattern: true }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + expect(body.destIndexDeleted.success).to.eql(true); + expect(body.destIndexPatternDeleted.success).to.eql(true); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + await ml.api.assertIndicesNotToExist(destinationIndex); + await ml.testResources.assertIndexPatternNotExist(destinationIndex); + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index 9e0f952ad501b..6693561076fdd 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('data frame analytics', function () { loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./delete')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 897f37821001e..fc2ce4bb16b99 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -9,9 +9,9 @@ import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/applicat import { FtrProviderContext } from '../../ftr_provider_context'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; +import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; @@ -110,6 +110,21 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, + async createIndices(indices: string) { + log.debug(`Creating indices: '${indices}'...`); + if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) { + log.debug(`Indices '${indices}' already exist. Nothing to create.`); + return; + } + + const createResponse = await es.indices.create({ index: indices }); + expect(createResponse) + .to.have.property('acknowledged') + .eql(true, 'Response for create request indices should be acknowledged.'); + + await this.assertIndicesExist(indices); + }, + async deleteIndices(indices: string) { log.debug(`Deleting indices: '${indices}'...`); if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { @@ -122,15 +137,9 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); expect(deleteResponse) .to.have.property('acknowledged') - .eql(true, 'Response for delete request should be acknowledged'); + .eql(true, 'Response for delete request should be acknowledged.'); - await retry.waitForWithTimeout(`'${indices}' indices to be deleted`, 30 * 1000, async () => { - if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { - return true; - } else { - throw new Error(`expected indices '${indices}' to be deleted`); - } - }); + await this.assertIndicesNotToExist(indices); }, async cleanMlIndices() { @@ -251,6 +260,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async assertIndicesNotToExist(indices: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { + return true; + } else { + throw new Error(`indices '${indices}' should not exist`); + } + }); + }, + async assertIndicesNotEmpty(indices: string) { await retry.tryForTime(30 * 1000, async () => { const response = await es.search({ @@ -394,9 +413,9 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { await this.waitForJobState(jobConfig.job_id, JOB_STATE.CLOSED); }, - async getDataFrameAnalyticsJob(analyticsId: string) { + async getDataFrameAnalyticsJob(analyticsId: string, statusCode = 200) { log.debug(`Fetching data frame analytics job '${analyticsId}'...`); - return await esSupertest.get(`/_ml/data_frame/analytics/${analyticsId}`).expect(200); + return await esSupertest.get(`/_ml/data_frame/analytics/${analyticsId}`).expect(statusCode); }, async waitForDataFrameAnalyticsJobToExist(analyticsId: string) { @@ -409,6 +428,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async waitForDataFrameAnalyticsJobNotToExist(analyticsId: string) { + await retry.waitForWithTimeout(`'${analyticsId}' not to exist`, 5 * 1000, async () => { + if (await this.getDataFrameAnalyticsJob(analyticsId, 404)) { + return true; + } else { + throw new Error(`expected data frame analytics job '${analyticsId}' not to exist`); + } + }); + }, + async createDataFrameAnalyticsJob(jobConfig: DataFrameAnalyticsConfig) { const { id: analyticsId, ...analyticsConfig } = jobConfig; log.debug(`Creating data frame analytic job with id '${analyticsId}'...`); diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index d349416ec90f7..739fd844f1193 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -5,7 +5,6 @@ */ import { ProvidedType } from '@kbn/test/types/ftr'; - import { savedSearches } from './test_resources_data'; import { COMMON_REQUEST_HEADERS } from './common'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -24,6 +23,7 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider const kibanaServer = getService('kibanaServer'); const log = getService('log'); const supertest = getService('supertest'); + const retry = getService('retry'); return { async setKibanaTimeZoneToUTC() { @@ -98,6 +98,21 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider } }, + async assertIndexPatternNotExist(title: string) { + await retry.waitForWithTimeout( + `index pattern '${title}' to not exist`, + 5 * 1000, + async () => { + const indexPatternId = await this.getIndexPatternId(title); + if (!indexPatternId) { + return true; + } else { + throw new Error(`Index pattern '${title}' should not exist.`); + } + } + ); + }, + async createSavedSearch(title: string, body: object): Promise { log.debug(`Creating saved search with title '${title}'`); From daf26b90667677b310ca0d0ae28be47c5d58a734 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Mon, 1 Jun 2020 11:13:59 -0500 Subject: [PATCH 27/69] [ML] Add minor refresh button to DFA and AD Job Messages tabs (#67750) * [ML] Add minor refresh button to DFA and AD Job Messages tabs * [ML] Update refresh logic for DFA [ML] Update refresh logic for DFA * [ML] Update fetchMessages callback Co-authored-by: Elastic Machine --- .../components/job_messages/job_messages.tsx | 23 ++++++++++++++++--- .../expanded_row_messages_pane.tsx | 11 ++++++--- .../job_details/job_messages_pane.tsx | 15 ++++++++---- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 9cea47ded09b4..fd2b7902833a6 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; -import { EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; +import { EuiSpacer, EuiInMemoryTable, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; @@ -21,16 +21,33 @@ interface JobMessagesProps { messages: JobMessage[]; loading: boolean; error: string; + refreshMessage?: React.MouseEventHandler; } /** * Component for rendering job messages for anomaly detection * and data frame analytics jobs. */ -export const JobMessages: FC = ({ messages, loading, error }) => { +export const JobMessages: FC = ({ messages, loading, error, refreshMessage }) => { const columns = [ { - name: '', + name: refreshMessage ? ( + + + + ) : ( + '' + ), render: (message: JobMessage) => , width: `${theme.euiSizeL}`, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index fc860251bf83d..0dd9eba172e1c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -22,7 +22,6 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { const getMessagesFactory = () => { let concurrentLoads = 0; - return async function getMessages() { try { concurrentLoads++; @@ -52,8 +51,14 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { } }; }; - useRefreshAnalyticsList({ onRefresh: getMessagesFactory() }); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index fbb64db94cd56..486de90d2299c 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState } from 'react'; - +import React, { FC, useCallback, useEffect, useState } from 'react'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import { JobMessage } from '../../../../../../common/types/audit_message'; - interface JobMessagesPaneProps { jobId: string; } @@ -32,9 +30,18 @@ export const JobMessagesPane: FC = ({ jobId }) => { } }; + const refreshMessage = useCallback(fetchMessages, [jobId]); + useEffect(() => { fetchMessages(); }, []); - return ; + return ( + + ); }; From ce47ef5d24656867121e8bd2c88cd3a86286e4c4 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 1 Jun 2020 10:09:07 -0700 Subject: [PATCH 28/69] Updating the licensed feature usage API response format (#67712) Co-authored-by: Elastic Machine --- .../licensing/server/routes/feature_usage.ts | 16 ++--- .../services/feature_usage_service.test.ts | 70 ++++++++++++------- .../server/services/feature_usage_service.ts | 37 ++++++---- .../feature_usage_test/server/plugin.ts | 6 +- .../licensed_feature_usage/feature_usage.ts | 25 +++++-- 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/licensing/server/routes/feature_usage.ts b/x-pack/plugins/licensing/server/routes/feature_usage.ts index 5fbfbc3f577b8..fa26d09903dc3 100644 --- a/x-pack/plugins/licensing/server/routes/feature_usage.ts +++ b/x-pack/plugins/licensing/server/routes/feature_usage.ts @@ -15,15 +15,13 @@ export function registerFeatureUsageRoute( async (context, request, response) => { const [, , { featureUsage }] = await getStartServices(); return response.ok({ - body: [...featureUsage.getLastUsages().entries()].reduce( - (res, [featureName, lastUsage]) => { - return { - ...res, - [featureName]: new Date(lastUsage).toISOString(), - }; - }, - {} - ), + body: { + features: featureUsage.getLastUsages().map((usage) => ({ + name: usage.name, + last_used: usage.lastUsed, + license_level: usage.licenseType, + })), + }, }); } ); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts index f0ef0dbec0b22..39f7aa6503b35 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts @@ -17,16 +17,13 @@ describe('FeatureUsageService', () => { jest.restoreAllMocks(); }); - const toObj = (map: ReadonlyMap): Record => - Object.fromEntries(map.entries()); - describe('#setup', () => { describe('#register', () => { it('throws when registering the same feature twice', () => { const setup = service.setup(); - setup.register('foo'); + setup.register('foo', 'basic'); expect(() => { - setup.register('foo'); + setup.register('foo', 'basic'); }).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); }); }); @@ -36,32 +33,50 @@ describe('FeatureUsageService', () => { describe('#notifyUsage', () => { it('allows to notify a feature usage', () => { const setup = service.setup(); - setup.register('feature'); + setup.register('feature', 'basic'); const start = service.start(); start.notifyUsage('feature', 127001); - expect(start.getLastUsages().get('feature')).toBe(127001); + expect(start.getLastUsages()).toEqual([ + { + lastUsed: new Date(127001), + licenseType: 'basic', + name: 'feature', + }, + ]); }); it('can receive a Date object', () => { const setup = service.setup(); - setup.register('feature'); + setup.register('feature', 'basic'); const start = service.start(); const usageTime = new Date(2015, 9, 21, 17, 54, 12); start.notifyUsage('feature', usageTime); - expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime()); + expect(start.getLastUsages()).toEqual([ + { + lastUsed: usageTime, + licenseType: 'basic', + name: 'feature', + }, + ]); }); it('uses the current time when `usedAt` is unspecified', () => { jest.spyOn(Date, 'now').mockReturnValue(42); const setup = service.setup(); - setup.register('feature'); + setup.register('feature', 'basic'); const start = service.start(); start.notifyUsage('feature'); - expect(start.getLastUsages().get('feature')).toBe(42); + expect(start.getLastUsages()).toEqual([ + { + lastUsed: new Date(42), + licenseType: 'basic', + name: 'feature', + }, + ]); }); it('throws when notifying for an unregistered feature', () => { @@ -76,40 +91,41 @@ describe('FeatureUsageService', () => { describe('#getLastUsages', () => { it('returns the last usage for all used features', () => { const setup = service.setup(); - setup.register('featureA'); - setup.register('featureB'); + setup.register('featureA', 'basic'); + setup.register('featureB', 'gold'); const start = service.start(); start.notifyUsage('featureA', 127001); start.notifyUsage('featureB', 6666); - expect(toObj(start.getLastUsages())).toEqual({ - featureA: 127001, - featureB: 6666, - }); + expect(start.getLastUsages()).toEqual([ + { lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' }, + { lastUsed: new Date(6666), licenseType: 'gold', name: 'featureB' }, + ]); }); it('returns the last usage even after notifying for an older usage', () => { const setup = service.setup(); - setup.register('featureA'); + setup.register('featureA', 'basic'); const start = service.start(); start.notifyUsage('featureA', 1000); start.notifyUsage('featureA', 500); - expect(toObj(start.getLastUsages())).toEqual({ - featureA: 1000, - }); + expect(start.getLastUsages()).toEqual([ + { lastUsed: new Date(1000), licenseType: 'basic', name: 'featureA' }, + ]); }); - it('does not return entries for unused registered features', () => { + it('returns entries for unused registered features', () => { const setup = service.setup(); - setup.register('featureA'); - setup.register('featureB'); + setup.register('featureA', 'basic'); + setup.register('featureB', 'gold'); const start = service.start(); start.notifyUsage('featureA', 127001); - expect(toObj(start.getLastUsages())).toEqual({ - featureA: 127001, - }); + expect(start.getLastUsages()).toEqual([ + { lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' }, + { lastUsed: null, licenseType: 'gold', name: 'featureB' }, + ]); }); }); }); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.ts index 0c6613d37f63a..9bfcb28f36b2a 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.ts @@ -5,13 +5,20 @@ */ import { isDate } from 'lodash'; +import { LicenseType } from '../../common/types'; /** @public */ export interface FeatureUsageServiceSetup { /** * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. */ - register(featureName: string): void; + register(featureName: string, licenseType: LicenseType): void; +} + +export interface LastFeatureUsage { + name: string; + lastUsed: Date | null; + licenseType: LicenseType; } /** @public */ @@ -27,20 +34,23 @@ export interface FeatureUsageServiceStart { * Return a map containing last usage timestamp for all features. * Features that were not used yet do not appear in the map. */ - getLastUsages(): ReadonlyMap; + getLastUsages(): LastFeatureUsage[]; } export class FeatureUsageService { - private readonly features: string[] = []; - private readonly lastUsages = new Map(); + private readonly lastUsages = new Map(); public setup(): FeatureUsageServiceSetup { return { - register: (featureName) => { - if (this.features.includes(featureName)) { + register: (featureName, licenseType) => { + if (this.lastUsages.has(featureName)) { throw new Error(`Feature '${featureName}' has already been registered.`); } - this.features.push(featureName); + this.lastUsages.set(featureName, { + name: featureName, + lastUsed: null, + licenseType, + }); }, }; } @@ -48,16 +58,17 @@ export class FeatureUsageService { public start(): FeatureUsageServiceStart { return { notifyUsage: (featureName, usedAt = Date.now()) => { - if (!this.features.includes(featureName)) { + const usage = this.lastUsages.get(featureName); + if (!usage) { throw new Error(`Feature '${featureName}' is not registered.`); } - if (isDate(usedAt)) { - usedAt = usedAt.getTime(); + + const lastUsed = isDate(usedAt) ? usedAt : new Date(usedAt); + if (usage.lastUsed == null || lastUsed > usage.lastUsed) { + usage.lastUsed = lastUsed; } - const currentValue = this.lastUsages.get(featureName) ?? 0; - this.lastUsages.set(featureName, Math.max(usedAt, currentValue)); }, - getLastUsages: () => new Map(this.lastUsages.entries()), + getLastUsages: () => Array.from(this.lastUsages.values()), }; } } diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts index b36d6dca077f7..af410d457fc05 100644 --- a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts @@ -38,9 +38,9 @@ export class FeatureUsageTestPlugin }: CoreSetup, { licensing }: FeatureUsageTestSetupDependencies ) { - licensing.featureUsage.register('test_feature_a'); - licensing.featureUsage.register('test_feature_b'); - licensing.featureUsage.register('test_feature_c'); + licensing.featureUsage.register('Test feature A', 'basic'); + licensing.featureUsage.register('Test feature B', 'gold'); + licensing.featureUsage.register('Test feature C', 'platinum'); registerRoutes(http.createRouter(), getStartServices); diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts index dfbc41d883e02..5c8fac9586e3a 100644 --- a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts @@ -20,15 +20,32 @@ export default function ({ getService }: FtrProviderContext) { describe('/api/licensing/feature_usage', () => { it('returns a map of last feature usages', async () => { const timeA = Date.now(); - await notifyUsage('test_feature_a', timeA); + await notifyUsage('Test feature C', timeA); const timeB = Date.now() - 4567; - await notifyUsage('test_feature_b', timeB); + await notifyUsage('Test feature B', timeB); const response = await supertest.get('/api/licensing/feature_usage').expect(200); - expect(response.body.test_feature_a).to.eql(toISO(timeA)); - expect(response.body.test_feature_b).to.eql(toISO(timeB)); + expect(response.body).to.eql({ + features: [ + { + last_used: null, + license_level: 'basic', + name: 'Test feature A', + }, + { + last_used: toISO(timeB), + license_level: 'gold', + name: 'Test feature B', + }, + { + last_used: toISO(timeA), + license_level: 'platinum', + name: 'Test feature C', + }, + ], + }); }); }); } From be51ca6041f77c48c53d6dd4ac30b2510bb73da3 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 1 Jun 2020 13:33:23 -0400 Subject: [PATCH 29/69] [Lens] Allow visualizations to provide a dimension editor (#67560) * [Lens] Allow visualizations to provide a dimension editor * Update to tab style * Remove table update * Update class name * typecheck fix * Add test * Require each dimension group to enable editor Co-authored-by: Elastic Machine Co-authored-by: Marta Bondyra --- .../config_panel/_layer_panel.scss | 3 + .../config_panel/config_panel.tsx | 3 - .../config_panel/dimension_popover.tsx | 2 +- .../config_panel/layer_panel.test.tsx | 271 ++++++++++++++++++ .../editor_frame/config_panel/layer_panel.tsx | 263 ++++++++++------- .../editor_frame/config_panel/types.ts | 1 + x-pack/plugins/lens/public/types.ts | 21 ++ 7 files changed, 458 insertions(+), 106 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index 3fbc42f9a25a0..924f44a37c459 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -31,3 +31,6 @@ min-height: $euiSizeXXL; } +.lnsLayerPanel__styleEditor { + width: $euiSize * 28; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 0d86a051b0faa..e53e465c18950 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -45,7 +45,6 @@ function LayerPanels( } ) { const { - framePublicAPI, activeVisualization, visualizationState, dispatch, @@ -109,12 +108,10 @@ function LayerPanels( {...props} key={layerId} layerId={layerId} - activeVisualization={activeVisualization} visualizationState={visualizationState} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateAll={updateAll} - frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} onRemoveLayer={() => { dispatch({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx index f89b6ef32d3f7..cc8d97a445016 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx @@ -36,7 +36,7 @@ export function DimensionPopover({ (popoverState.openId === accessor || (noMatch && popoverState.addingToGroupId === groupId)) } closePopover={() => { - setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null, tabId: null }); }} button={trigger} anchorPosition="leftUp" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx new file mode 100644 index 0000000000000..1f987f86d3950 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, + DatasourceMock, +} from '../../mocks'; +import { EuiFormRow, EuiPopover } from '@elastic/eui'; +import { mount } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { Visualization } from '../../../types'; +import { LayerPanel } from './layer_panel'; +import { coreMock } from 'src/core/public/mocks'; +import { generateId } from '../../../id_generator'; + +jest.mock('../../../id_generator'); + +describe('LayerPanel', () => { + let mockVisualization: jest.Mocked; + let mockDatasource: DatasourceMock; + + function getDefaultProps() { + const frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + return { + layerId: 'first', + activeVisualizationId: 'vis1', + visualizationMap: { + vis1: mockVisualization, + }, + activeDatasourceId: 'ds1', + datasourceMap: { + ds1: mockDatasource, + }, + datasourceStates: { + ds1: { + isLoading: false, + state: 'state', + }, + }, + visualizationState: 'state', + updateVisualization: jest.fn(), + updateDatasource: jest.fn(), + updateAll: jest.fn(), + framePublicAPI: frame, + isOnlyLayer: true, + onRemoveLayer: jest.fn(), + dispatch: jest.fn(), + core: coreMock.createStart(), + }; + } + + beforeEach(() => { + mockVisualization = { + ...createMockVisualization(), + id: 'testVis', + visualizationTypes: [ + { + icon: 'empty', + id: 'testVis', + label: 'TEST1', + }, + ], + }; + + mockVisualization.getLayerIds.mockReturnValue(['first']); + mockDatasource = createMockDatasource('ds1'); + }); + + it('should fail to render if the public API is out of date', () => { + const props = getDefaultProps(); + props.framePublicAPI.datasourceLayers = {}; + const component = mountWithIntl(); + expect(component.isEmptyRender()).toBe(true); + }); + + it('should fail to render if the active visualization is missing', () => { + const component = mountWithIntl( + + ); + expect(component.isEmptyRender()).toBe(true); + }); + + describe('layer reset and remove', () => { + it('should show the reset button when single layer', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + 'Reset layer' + ); + }); + + it('should show the delete button when multiple layers', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + 'Delete layer' + ); + }); + + it('should call the clear callback', () => { + const cb = jest.fn(); + const component = mountWithIntl(); + act(() => { + component.find('[data-test-subj="lns_layer_remove"]').first().simulate('click'); + }); + expect(cb).toHaveBeenCalled(); + }); + }); + + describe('single group', () => { + it('should render the non-editable state', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['x'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); + + it('should render the group with a way to add a new column', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); + + it('should render the required warning when only one group is configured', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['x'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + required: true, + }, + ], + }); + + const component = mountWithIntl(); + + const group = component + .find(EuiFormRow) + .findWhere((e) => e.prop('error') === 'Required dimension'); + expect(group).toHaveLength(1); + }); + + it('should render the datasource and visualization panels inside the dimension popover', () => { + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + enableDimensionEditor: true, + }, + ], + }); + mockVisualization.renderDimensionEditor = jest.fn(); + + const component = mountWithIntl(); + + const group = component.find('DimensionPopover'); + const panel = mount(group.prop('panel')); + + expect(panel.find('EuiTabbedContent').prop('tabs')).toHaveLength(2); + act(() => { + panel.find('EuiTab#visualization').simulate('click'); + }); + expect(mockVisualization.renderDimensionEditor).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + groupId: 'a', + accessor: 'newid', + }) + ); + }); + + it('should keep the popover open when configuring a new dimension', () => { + /** + * The ID generation system for new dimensions has been messy before, so + * this tests that the ID used in the first render is used to keep the popover + * open in future renders + */ + (generateId as jest.Mock).mockReturnValueOnce(`newid`); + (generateId as jest.Mock).mockReturnValueOnce(`bad`); + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + // Normally the configuration would change in response to a state update, + // but this test is updating it directly + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DimensionPopover'); + const triggerButton = mountWithIntl(group.prop('trigger')); + act(() => { + triggerButton.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 814b7fc644c9c..bd501db2b752a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -13,11 +13,12 @@ import { EuiFlexItem, EuiButtonEmpty, EuiFormRow, + EuiTabbedContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NativeRenderer } from '../../../native_renderer'; -import { Visualization, FramePublicAPI, StateSetter } from '../../../types'; +import { StateSetter } from '../../../types'; import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -27,11 +28,8 @@ import { DimensionPopover } from './dimension_popover'; export function LayerPanel( props: Exclude & { - frame: FramePublicAPI; layerId: string; isOnlyLayer: boolean; - activeVisualization: Visualization; - visualizationState: unknown; updateVisualization: StateSetter; updateDatasource: (datasourceId: string, newState: unknown) => void; updateAll: ( @@ -47,13 +45,19 @@ export function LayerPanel( isOpen: false, openId: null, addingToGroupId: null, + tabId: null, }); - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; + const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - if (!datasourcePublicAPI) { + if ( + !datasourcePublicAPI || + !props.activeVisualizationId || + !props.visualizationMap[props.activeVisualizationId] + ) { return null; } + const activeVisualization = props.visualizationMap[props.activeVisualizationId]; const layerVisualizationConfigProps = { layerId, dragDropContext, @@ -158,104 +162,156 @@ export function LayerPanel( } > <> - {group.accessors.map((accessor) => ( - { - layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - }} - > - { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: accessor, - addingToGroupId: null, // not set for existing dimension - }); - } - }, - }} - /> - } - panel={ - - } - /> + {group.accessors.map((accessor) => { + const tabs = [ + { + id: 'datasource', + name: i18n.translate('xpack.lens.editorFrame.quickFunctionsLabel', { + defaultMessage: 'Quick functions', + }), + content: ( + <> + + + + ), + }, + ]; - { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: accessor, - prevState: layerDatasourceState, - }), - props.activeVisualization.removeDimension({ - layerId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); + if (activeVisualization.renderDimensionEditor) { + tabs.push({ + id: 'visualization', + name: i18n.translate('xpack.lens.editorFrame.formatStyleLabel', { + defaultMessage: 'Format & style', + }), + content: ( +
    + + +
    + ), + }); + } + + return ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); }} - /> - - ))} + > + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + tabId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + tabId: 'datasource', + }); + } + }, + }} + /> + } + panel={ + t.id === popoverState.tabId)} + size="s" + onTabClick={(tab) => { + setPopoverState({ + ...popoverState, + tabId: tab.id as typeof popoverState['tabId'], + }); + }} + /> + } + /> + + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> +
    + ); + })} {group.supportsMoreColumns ? ( = VisualizationConfigProp setState: (newState: T) => void; }; +export type VisualizationDimensionEditorProps = VisualizationConfigProps & { + groupId: string; + accessor: string; + setState: (newState: T) => void; +}; + export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; @@ -300,6 +306,12 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { /** If required, a warning will appear if accessors are empty */ required?: boolean; dataTestSubj?: string; + + /** + * When the dimension editor is enabled for this group, all dimensions in the group + * will render the extra tab for the dimension editor + */ + enableDimensionEditor?: boolean; }; interface VisualizationDimensionChangeProps { @@ -459,6 +471,15 @@ export interface Visualization { */ removeDimension: (props: VisualizationDimensionChangeProps) => T; + /** + * Additional editor that gets rendered inside the dimension popover. + * This can be used to configure dimension-specific options + */ + renderDimensionEditor?: ( + domElement: Element, + props: VisualizationDimensionEditorProps + ) => void; + /** * The frame will call this function on all visualizations at different times. The * main use cases where visualization suggestions are requested are: From 5a6c77226c212ad51ce84a23d81c8781c6af8d72 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 1 Jun 2020 18:37:56 +0100 Subject: [PATCH 30/69] skip flaky suite (#67833) --- .../__jest__/client_integration/template_create.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx index 05abe284fab32..8f464987418c0 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx @@ -59,7 +59,8 @@ const KEYWORD_MAPPING_FIELD = { type: 'keyword', }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/67833 +describe.skip('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); From cdbcb9720b69d6373dbe8da06bd15d9d0324e795 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 1 Jun 2020 19:53:16 +0200 Subject: [PATCH 31/69] [Uptime] Use date histogram in monitor states (#67558) Co-authored-by: Elastic Machine --- .../server/lib/requests/search/enrich_monitor_groups.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 9f153e186420d..53b1fe881cd92 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -15,6 +15,7 @@ import { SortOrder, } from '../../../../common/runtime_types'; import { MonitorEnricher } from './fetch_page'; +import { getHistogramInterval } from '../../helper/get_histogram_interval'; export const enrichMonitorGroups: MonitorEnricher = async ( queryContext: QueryContext, @@ -317,11 +318,13 @@ const getHistogramForMonitors = async ( }, aggs: { histogram: { - auto_date_histogram: { + date_histogram: { field: '@timestamp', // 12 seems to be a good size for performance given // long monitor lists of up to 100 on the overview page - buckets: 12, + fixed_interval: + getHistogramInterval(queryContext.dateRangeStart, queryContext.dateRangeEnd, 12) + + 'ms', missing: 0, }, aggs: { From 279b11b78d22b385d152fb66eee0dfa979366ce0 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 1 Jun 2020 14:32:42 -0400 Subject: [PATCH 32/69] [SIEM][Exceptions] - Update exceptions hooks to include _find filtering (#67435) ### Summary - Updates exception list hooks to include filtering options and updates corresponding unit tests. - Adds refreshList callback to hook that fetches the list and its items - Updates hooks tests to test onError callback - Updates tests to use type checking more effectively per feedback from @FrankHassanabad (thanks!) --- x-pack/plugins/lists/common/constants.mock.ts | 10 + x-pack/plugins/lists/common/constants.ts | 6 + .../create_exception_list_item_schema.mock.ts | 34 +++ .../lists/public/exceptions/__mocks__/api.ts | 2 + .../lists/public/exceptions/api.test.ts | 220 +++++++++++++----- x-pack/plugins/lists/public/exceptions/api.ts | 69 +++++- .../hooks/persist_exception_item.test.tsx | 56 +++-- .../hooks/persist_exception_item.tsx | 7 + .../hooks/persist_exception_list.test.tsx | 53 +++-- .../hooks/persist_exception_list.tsx | 7 + .../hooks/use_exception_list.test.tsx | 200 ++++++++++------ .../exceptions/hooks/use_exception_list.tsx | 132 ++++++++--- .../plugins/lists/public/exceptions/types.ts | 27 ++- 13 files changed, 627 insertions(+), 196 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 8c5f6b0cbe56c..d8e4dfba1599e 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -29,3 +29,13 @@ export const TYPE = 'ip'; export const VALUE = '127.0.0.1'; export const VALUE_2 = '255.255.255'; export const NAMESPACE_TYPE = 'single'; + +// Exception List specific +export const ENDPOINT_TYPE = 'endpoint'; +export const ENTRIES = [ + { field: 'some.field', match: 'some value', match_any: undefined, operator: 'included' }, +]; +export const ITEM_TYPE = 'simple'; +export const _TAGS = []; +export const TAGS = []; +export const COMMENT = []; diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index 96d28bf618ce4..6cb88b19483ce 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -16,3 +16,9 @@ export const LIST_ITEM_URL = `${LIST_URL}/items`; */ export const EXCEPTION_LIST_URL = '/api/exception_lists'; export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; + +/** + * Exception list spaces + */ +export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; +export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f9af10245b7ee --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + COMMENT, + DESCRIPTION, + ENTRIES, + ITEM_TYPE, + LIST_ID, + META, + NAME, + NAMESPACE_TYPE, + TAGS, + _TAGS, +} from '../../constants.mock'; + +import { CreateExceptionListItemSchema } from './create_exception_list_item_schema'; + +export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({ + _tags: _TAGS, + comment: COMMENT, + description: DESCRIPTION, + entries: ENTRIES, + item_id: undefined, + list_id: LIST_ID, + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + tags: TAGS, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts index 787d374ab2cad..ecc771279b3ab 100644 --- a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts +++ b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts @@ -40,8 +40,10 @@ export const fetchExceptionListById = async ({ }: ApiCallByIdProps): Promise => Promise.resolve(getExceptionListSchemaMock()); export const fetchExceptionListItemsByListId = async ({ + filterOptions, http, listId, + pagination, signal, }: ApiCallByListIdProps): Promise => Promise.resolve({ data: [getExceptionListItemSchemaMock()], page: 1, per_page: 20, total: 1 }); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 18a89071e9887..b9512bb398745 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -6,8 +6,9 @@ import { createKibanaCoreStartMock } from '../common/mocks/kibana_core'; import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock'; import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListSchemaMock } from '../../common/schemas/request/create_exception_list_schema.mock'; +import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/request/create_exception_list_item_schema.mock'; -import { mockNewExceptionItem, mockNewExceptionList } from './mock'; import { addExceptionList, addExceptionListItem, @@ -37,188 +38,291 @@ const mockKibanaHttpService = ((createKibanaCoreStartMock() as unknown) as jest. ); describe('Exceptions Lists API', () => { - describe('addExceptionList', () => { + describe('#addExceptionList', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); - test('check parameter url, body', async () => { - await addExceptionList({ + test('it uses POST when "list.id" does not exist', async () => { + const payload = getCreateExceptionListSchemaMock(); + const exceptionResponse = await addExceptionList({ http: mockKibanaHttpService(), - list: mockNewExceptionList, + list: payload, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { - body: - '{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"endpoint"}', + body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, }); + expect(exceptionResponse).toEqual({ id: '1', ...getExceptionListSchemaMock() }); }); - test('check parameter url, body when "list.id" exists', async () => { - await addExceptionList({ + test('it uses PUT when "list.id" exists', async () => { + const payload = getExceptionListSchemaMock(); + const exceptionResponse = await addExceptionList({ http: mockKibanaHttpService(), list: getExceptionListSchemaMock(), signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { - body: - '{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}', + body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, }); - }); - - test('happy path', async () => { - const exceptionResponse = await addExceptionList({ - http: mockKibanaHttpService(), - list: mockNewExceptionList, - signal: abortCtrl.signal, - }); expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); - describe('addExceptionListItem', () => { + describe('#addExceptionListItem', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); - test('check parameter url, body', async () => { - await addExceptionListItem({ + test('it uses POST when "listItem.id" does not exist', async () => { + const payload = getCreateExceptionListItemSchemaMock(); + const exceptionResponse = await addExceptionListItem({ http: mockKibanaHttpService(), - listItem: mockNewExceptionItem, + listItem: payload, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { - body: - '{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"item_id":"endpoint_list_item","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"simple"}', + body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, }); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); test('check parameter url, body when "listItem.id" exists', async () => { - await addExceptionListItem({ + const payload = getExceptionListItemSchemaMock(); + const exceptionResponse = await addExceptionListItem({ http: mockKibanaHttpService(), listItem: getExceptionListItemSchemaMock(), signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { - body: - '{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}', + body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, }); - }); - - test('happy path', async () => { - const exceptionResponse = await addExceptionListItem({ - http: mockKibanaHttpService(), - listItem: mockNewExceptionItem, - signal: abortCtrl.signal, - }); expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); - describe('fetchExceptionListById', () => { + describe('#fetchExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); - test('check parameter url, body', async () => { + test('it invokes "fetchExceptionListById" with expected url and body values', async () => { await fetchExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { method: 'GET', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected exception list on success', async () => { const exceptionResponse = await fetchExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); - describe('fetchExceptionListItemsByListId', () => { + describe('#fetchExceptionListItemsByListId', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue([mockNewExceptionItem]); + fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]); }); - test('check parameter url, body', async () => { + test('it invokes "fetchExceptionListItemsByListId" with expected url and body values', async () => { await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), - listId: 'endpoint_list', + listId: 'myList', + namespaceType: 'single', + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + list_id: 'myList', + namespace_type: 'single', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: 'hello world', + tags: [], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'single', signal: abortCtrl.signal, }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + filter: 'exception-list.attributes.entries.field:hello world*', + list_id: 'myList', + namespace_type: 'single', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: 'hello world', + tags: [], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + filter: 'exception-list-agnostic.attributes.entries.field:hello world*', + list_id: 'myList', + namespace_type: 'agnostic', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when tags exists', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: '', + tags: ['malware'], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + filter: 'exception-list-agnostic.attributes.tags:malware', + list_id: 'myList', + namespace_type: 'agnostic', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when filter and tags exists', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: 'host.name', + tags: ['malware'], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { - list_id: 'endpoint_list', + filter: + 'exception-list-agnostic.attributes.entries.field:host.name* AND exception-list-agnostic.attributes.tags:malware', + list_id: 'myList', + namespace_type: 'agnostic', + page: 1, + per_page: 20, }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), listId: 'endpoint_list', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual([mockNewExceptionItem]); + expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]); }); }); - describe('fetchExceptionListItemById', () => { + describe('#fetchExceptionListItemById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue([mockNewExceptionItem]); + fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]); }); - test('check parameter url, body', async () => { + test('it invokes "fetchExceptionListItemById" with expected url and body values', async () => { await fetchExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'GET', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual([mockNewExceptionItem]); + expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]); }); }); - describe('deleteExceptionListById', () => { + describe('#deleteExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getExceptionListSchemaMock()); @@ -228,28 +332,31 @@ describe('Exceptions Lists API', () => { await deleteExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { method: 'DELETE', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); - describe('deleteExceptionListItemById', () => { + describe('#deleteExceptionListItemById', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); @@ -259,21 +366,24 @@ describe('Exceptions Lists API', () => { await deleteExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'DELETE', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index fdd9d62539e06..6968ba5f50e72 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../common/constants'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_NAMESPACE, + EXCEPTION_LIST_NAMESPACE_AGNOSTIC, + EXCEPTION_LIST_URL, +} from '../../common/constants'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -21,6 +26,7 @@ import { /** * Add provided ExceptionList * + * @param http Kibana http service * @param list exception list to add * @param signal to cancel request * @@ -43,6 +49,7 @@ export const addExceptionList = async ({ /** * Add provided ExceptionListItem * + * @param http Kibana http service * @param listItem exception list item to add * @param signal to cancel request * @@ -65,7 +72,9 @@ export const addExceptionListItem = async ({ /** * Fetch an ExceptionList by providing a ExceptionList ID * + * @param http Kibana http service * @param id ExceptionList ID (not list_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -73,18 +82,23 @@ export const addExceptionListItem = async ({ export const fetchExceptionListById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_URL}`, { method: 'GET', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); /** * Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id * - * @param id ExceptionList list_id (not ID) + * @param http Kibana http service + * @param listId ExceptionList list_id (not ID) + * @param namespaceType ExceptionList namespace_type + * @param filterOptions optional - filter by field or tags + * @param pagination optional * @param signal to cancel request * * @throws An error if response is not OK @@ -92,18 +106,48 @@ export const fetchExceptionListById = async ({ export const fetchExceptionListItemsByListId = async ({ http, listId, + namespaceType, + filterOptions = { + filter: '', + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, signal, -}: ApiCallByListIdProps): Promise => - http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { +}: ApiCallByListIdProps): Promise => { + const namespace = + namespaceType === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; + const filters = [ + ...(filterOptions.filter.length + ? [`${namespace}.attributes.entries.field:${filterOptions.filter}*`] + : []), + ...(filterOptions.tags?.map((t) => `${namespace}.attributes.tags:${t}`) ?? []), + ]; + + const query = { + list_id: listId, + namespace_type: namespaceType, + page: pagination.page, + per_page: pagination.perPage, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; + + return http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { method: 'GET', - query: { list_id: listId }, + query, signal, }); +}; /** * Fetch an ExceptionListItem by providing a ExceptionListItem ID * + * @param http Kibana http service * @param id ExceptionListItem ID (not item_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -111,18 +155,21 @@ export const fetchExceptionListItemsByListId = async ({ export const fetchExceptionListItemById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_ITEM_URL}`, { method: 'GET', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); /** * Delete an ExceptionList by providing a ExceptionList ID * + * @param http Kibana http service * @param id ExceptionList ID (not list_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -130,18 +177,21 @@ export const fetchExceptionListItemById = async ({ export const deleteExceptionListById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_URL}`, { method: 'DELETE', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); /** * Delete an ExceptionListItem by providing a ExceptionListItem ID * + * @param http Kibana http service * @param id ExceptionListItem ID (not item_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -149,10 +199,11 @@ export const deleteExceptionListById = async ({ export const deleteExceptionListItemById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_ITEM_URL}`, { method: 'DELETE', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx index b78ad250b8910..1db18168b11fe 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx @@ -6,8 +6,10 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import * as api from '../api'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; +import { PersistHookProps } from '../types'; import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item'; @@ -16,38 +18,66 @@ jest.mock('../api'); const mockKibanaHttpService = createKibanaCoreStartMock().http; describe('usePersistExceptionItem', () => { - test('init', async () => { - const onError = jest.fn(); - const { result } = renderHook(() => + const onError = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + const { result } = renderHook(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }) ); expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); }); - test('saving exception item with isLoading === true', async () => { + test('"isLoading" is "true" when exception item is being saved', async () => { await act(async () => { - const onError = jest.fn(); - const { result, rerender, waitForNextUpdate } = renderHook( - () => usePersistExceptionItem({ http: mockKibanaHttpService, onError }) - ); + const { result, rerender, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionItem + >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })); + await waitForNextUpdate(); result.current[1](getExceptionListItemSchemaMock()); rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); }); - test('saved exception item with isSaved === true', async () => { - const onError = jest.fn(); + test('"isSaved" is "true" when exception item saved successfully', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePersistExceptionItem({ http: mockKibanaHttpService, onError }) - ); + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionItem + >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })); + await waitForNextUpdate(); result.current[1](getExceptionListItemSchemaMock()); await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); }); + + test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => { + const error = new Error('persist rule failed'); + jest.spyOn(api, 'addExceptionListItem').mockRejectedValue(error); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionItem + >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })); + + await waitForNextUpdate(); + result.current[1](getExceptionListItemSchemaMock()); + await waitForNextUpdate(); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + expect(onError).toHaveBeenCalledWith(error); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx index 0ed007e805013..d9fe3a82ac177 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx @@ -19,6 +19,13 @@ export type ReturnPersistExceptionItem = [ Dispatch ]; +/** + * Hook for creating or updating ExceptionListItem + * + * @param http Kibana http service + * @param onError error callback + * + */ export const usePersistExceptionItem = ({ http, onError, diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx index 605dd635aa4f5..80d6e27043c99 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx @@ -6,8 +6,10 @@ import { act, renderHook } from '@testing-library/react-hooks'; +import * as api from '../api'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; +import { PersistHookProps } from '../types'; import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list'; @@ -16,38 +18,63 @@ jest.mock('../api'); const mockKibanaHttpService = createKibanaCoreStartMock().http; describe('usePersistExceptionList', () => { - test('init', async () => { - const onError = jest.fn(); - const { result } = renderHook(() => + const onError = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + const { result } = renderHook(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }) ); expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); }); - test('saving exception list with isLoading === true', async () => { - const onError = jest.fn(); + test('"isLoading" is "true" when exception item is being saved', async () => { await act(async () => { - const { result, rerender, waitForNextUpdate } = renderHook( - () => usePersistExceptionList({ http: mockKibanaHttpService, onError }) - ); + const { result, rerender, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionList + >(() => usePersistExceptionList({ http: mockKibanaHttpService, onError })); await waitForNextUpdate(); result.current[1](getExceptionListSchemaMock()); rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); }); - test('saved exception list with isSaved === true', async () => { - const onError = jest.fn(); + test('"isSaved" is "true" when exception item saved successfully', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePersistExceptionList({ http: mockKibanaHttpService, onError }) - ); + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionList + >(() => usePersistExceptionList({ http: mockKibanaHttpService, onError })); await waitForNextUpdate(); result.current[1](getExceptionListSchemaMock()); await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); }); + + test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => { + const error = new Error('persist rule failed'); + jest.spyOn(api, 'addExceptionList').mockRejectedValue(error); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionList + >(() => usePersistExceptionList({ http: mockKibanaHttpService, onError })); + await waitForNextUpdate(); + result.current[1](getExceptionListSchemaMock()); + await waitForNextUpdate(); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + expect(onError).toHaveBeenCalledWith(error); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx index 45330c9725ae7..5848a17145194 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx @@ -19,6 +19,13 @@ export type ReturnPersistExceptionList = [ Dispatch ]; +/** + * Hook for creating or updating ExceptionList + * + * @param http Kibana http service + * @param onError error callback + * + */ export const usePersistExceptionList = ({ http, onError, diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx index 308d1cf4d1b17..a6a25ab4d4e9d 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx @@ -8,6 +8,9 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListAndItems, UseExceptionListProps } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; @@ -16,103 +19,166 @@ jest.mock('../api'); const mockKibanaHttpService = createKibanaCoreStartMock().http; describe('useExceptionList', () => { - test('init', async () => { - const onError = jest.fn(); + const onErrorMock = jest.fn(); + + afterEach(() => { + onErrorMock.mockClear(); + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError }) + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) ); await waitForNextUpdate(); - expect(result.current).toEqual([true, null]); + + expect(result.current).toEqual([true, null, result.current[2]]); + expect(typeof result.current[2]).toEqual('function'); }); }); test('fetch exception list and items', async () => { - const onError = jest.fn(); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError }) + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); - expect(result.current).toEqual([ - false, - { - _tags: ['endpoint', 'process', 'malware', 'os:linux'], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', - exceptionItems: { - data: [ - { - _tags: ['endpoint', 'process', 'malware', 'os:linux'], - comment: [], - created_at: '2020-04-23T00:19:13.289Z', - created_by: 'user_name', - description: 'This is a sample endpoint type exception', - entries: [ - { - field: 'actingProcess.file.signer', - match: 'Elastic, N.V.', - match_any: undefined, - operator: 'included', - }, - { - field: 'event.category', - match: undefined, - match_any: ['process', 'malware'], - operator: 'included', - }, - ], - id: '1', - item_id: 'endpoint_list_item', - list_id: 'endpoint_list', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', - tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'simple', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', - }, - ], + + const expectedResult: ExceptionListAndItems = { + ...getExceptionListSchemaMock(), + exceptionItems: { + items: [{ ...getExceptionListItemSchemaMock() }], + pagination: { page: 1, - per_page: 20, + perPage: 20, total: 1, }, - id: '1', - list_id: 'endpoint_list', - meta: {}, - name: 'Sample Endpoint Exception List', - namespace_type: 'single', - tags: ['user added string for a tag', 'malware'], - tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', - type: 'endpoint', - updated_at: '2020-04-23T00:19:13.289Z', - updated_by: 'user_name', }, - ]); + }; + + expect(result.current).toEqual([false, expectedResult, result.current[2]]); }); }); test('fetch a new exception list and its items', async () => { - const onError = jest.fn(); const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); await act(async () => { - const { rerender, waitForNextUpdate } = renderHook( - (id) => useExceptionList({ http: mockKibanaHttpService, id, onError }), + const { rerender, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >( + ({ filterOptions, http, id, namespaceType, pagination, onError }) => + useExceptionList({ filterOptions, http, id, namespaceType, onError, pagination }), { - initialProps: 'myListId', + initialProps: { + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }, } ); await waitForNextUpdate(); + rerender({ + http: mockKibanaHttpService, + id: 'newListId', + namespaceType: 'single', + onError: onErrorMock, + }); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); + expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); + }); + }); + + test('fetches list and items when refreshExceptionList callback invoked', async () => { + const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); + const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) + ); + await waitForNextUpdate(); await waitForNextUpdate(); - rerender('newListId'); + result.current[2](); await waitForNextUpdate(); + expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); }); }); + + test('invokes "onError" callback if "fetchExceptionListItemsByListId" fails', async () => { + const mockError = new Error('failed to fetch list items'); + const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); + const spyOnfetchExceptionListItemsByListId = jest + .spyOn(api, 'fetchExceptionListItemsByListId') + .mockRejectedValue(mockError); + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledWith(mockError); + expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(1); + }); + }); + + test('invokes "onError" callback if "fetchExceptionListById" fails', async () => { + const mockError = new Error('failed to fetch list'); + jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError); + + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx index d0ac357e05aa0..116233cd89348 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx @@ -4,66 +4,124 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api'; import { ExceptionListAndItems, UseExceptionListProps } from '../types'; -export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null]; +export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null, () => void]; /** * Hook for using to get an ExceptionList and it's ExceptionListItems * + * @param http Kibana http service * @param id desired ExceptionList ID (not list_id) + * @param namespaceType list namespaceType determines list space + * @param onError error callback + * @param filterOptions optional - filter by fields or tags + * @param pagination optional * */ export const useExceptionList = ({ http, id, + namespaceType, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + filterOptions = { + filter: '', + tags: [], + }, onError, }: UseExceptionListProps): ReturnExceptionListAndItems => { const [exceptionListAndItems, setExceptionList] = useState(null); + const [shouldRefresh, setRefresh] = useState(true); + const refreshExceptionList = useCallback(() => setRefresh(true), [setRefresh]); const [loading, setLoading] = useState(true); + const tags = filterOptions.tags.sort().join(); - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + useEffect( + () => { + let isSubscribed = true; + const abortCtrl = new AbortController(); - const fetchData = async (idToFetch: string): Promise => { - try { - setLoading(true); - const exceptionList = await fetchExceptionListById({ - http, - id: idToFetch, - signal: abortCtrl.signal, - }); - const exceptionListItems = await fetchExceptionListItemsByListId({ - http, - listId: exceptionList.list_id, - signal: abortCtrl.signal, - }); - if (isSubscribed) { - setExceptionList({ ...exceptionList, exceptionItems: { ...exceptionListItems } }); + const fetchData = async (idToFetch: string): Promise => { + if (shouldRefresh) { + try { + setLoading(true); + + const { + list_id, + namespace_type, + ...restOfExceptionList + } = await fetchExceptionListById({ + http, + id: idToFetch, + namespaceType, + signal: abortCtrl.signal, + }); + const fetchListItemsResult = await fetchExceptionListItemsByListId({ + filterOptions, + http, + listId: list_id, + namespaceType: namespace_type, + pagination, + signal: abortCtrl.signal, + }); + + setRefresh(false); + + if (isSubscribed) { + setExceptionList({ + list_id, + namespace_type, + ...restOfExceptionList, + exceptionItems: { + items: [...fetchListItemsResult.data], + pagination: { + page: fetchListItemsResult.page, + perPage: fetchListItemsResult.per_page, + total: fetchListItemsResult.total, + }, + }, + }); + } + } catch (error) { + setRefresh(false); + if (isSubscribed) { + setExceptionList(null); + onError(error); + } + } } - } catch (error) { + if (isSubscribed) { - setExceptionList(null); - onError(error); + setLoading(false); } - } - if (isSubscribed) { - setLoading(false); - } - }; + }; - if (id != null) { - fetchData(id); - } - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [http, id, onError]); + if (id != null) { + fetchData(id); + } + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, // eslint-disable-next-line react-hooks/exhaustive-deps + [ + http, + id, + onError, + shouldRefresh, + pagination.page, + pagination.perPage, + filterOptions.filter, + tags, + ] + ); - return [loading, exceptionListAndItems]; + return [loading, exceptionListAndItems, refreshExceptionList]; }; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index fcf2108e7323a..cf6b6c3ec1c59 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -9,12 +9,28 @@ import { CreateExceptionListSchemaPartial, ExceptionListItemSchema, ExceptionListSchema, - FoundExceptionListItemSchema, + NamespaceType, } from '../../common/schemas'; import { HttpStart } from '../../../../../src/core/public'; +export interface FilterExceptionsOptions { + filter: string; + tags: string[]; +} + +export interface Pagination { + page: number; + perPage: number; + total: number; +} + +export interface ExceptionItemsAndPagination { + items: ExceptionListItemSchema[]; + pagination: Pagination; +} + export interface ExceptionListAndItems extends ExceptionListSchema { - exceptionItems: FoundExceptionListItemSchema; + exceptionItems: ExceptionItemsAndPagination; } export type AddExceptionList = ExceptionListSchema | CreateExceptionListSchemaPartial; @@ -27,20 +43,27 @@ export interface PersistHookProps { } export interface UseExceptionListProps { + filterOptions?: FilterExceptionsOptions; http: HttpStart; id: string | undefined; + namespaceType: NamespaceType; onError: (arg: Error) => void; + pagination?: Pagination; } export interface ApiCallByListIdProps { http: HttpStart; listId: string; + namespaceType: NamespaceType; + filterOptions?: FilterExceptionsOptions; + pagination?: Pagination; signal: AbortSignal; } export interface ApiCallByIdProps { http: HttpStart; id: string; + namespaceType: NamespaceType; signal: AbortSignal; } From add5b11611973d58c5ed2806f525ab8a00250830 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 1 Jun 2020 14:41:30 -0400 Subject: [PATCH 33/69] [CI] Fix packer cache node_modules references --- .ci/packer_cache.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index ab68a60dcfc27..d47ef93172a9d 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -35,20 +35,20 @@ mkdir -p ".geckodriver" cp "node_modules/geckodriver/geckodriver.tar.gz" .geckodriver/geckodriver.tar.gz echo "$geckodriverPkgVersion" > .geckodriver/pkgVersion +echo "Creating bootstrap_cache archive" + # archive cacheable directories mkdir -p "$HOME/.kibana/bootstrap_cache" tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ - node_modules \ - packages/*/node_modules \ - x-pack/node_modules \ - x-pack/legacy/plugins/*/node_modules \ x-pack/legacy/plugins/reporting/.chromium \ - test/plugin_functional/plugins/*/node_modules \ - examples/*/node_modules \ .es \ .chromedriver \ .geckodriver; +echo "Adding node_modules" +# Find all of the node_modules directories that aren't test fixtures, and aren't inside other node_modules directories, and append them to the tar +find . -type d -name node_modules -not -path '*__fixtures__*' -prune -print0 | xargs -0I % tar -rf "$HOME/.kibana/bootstrap_cache/$branch.tar" "%" + echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" if [ "$branch" == "master" ]; then From b061d85f9a85d467beae8da9a0d46758d6e194eb Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 1 Jun 2020 15:05:27 -0400 Subject: [PATCH 34/69] [Lens] Warn if leaving with unsaved visualization (#67689) * [Lens] Warn if leaving with unsaved visualization * Made confirmation logic more robust and add title Co-authored-by: Elastic Machine --- .../lens/public/app_plugin/app.test.tsx | 216 +++++++++++++----- x-pack/plugins/lens/public/app_plugin/app.tsx | 57 ++++- .../lens/public/app_plugin/mounter.tsx | 1 + 3 files changed, 208 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 1762965478292..f1a2edd2d554f 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -9,6 +9,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { EditorFrameInstance } from '../types'; +import { AppMountParameters } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; @@ -111,6 +112,7 @@ describe('Lens App', () => { newlyCreated?: boolean ) => void; originatingApp: string | undefined; + onAppLeave: AppMountParameters['onAppLeave']; }> { return ({ navigation: navigationStartMock, @@ -153,6 +155,7 @@ describe('Lens App', () => { newlyCreated?: boolean ) => {} ), + onAppLeave: jest.fn(), } as unknown) as jest.Mocked<{ navigation: typeof navigationStartMock; editorFrame: EditorFrameInstance; @@ -168,6 +171,7 @@ describe('Lens App', () => { newlyCreated?: boolean ) => void; originatingApp: string | undefined; + onAppLeave: AppMountParameters['onAppLeave']; }>; } @@ -357,22 +361,7 @@ describe('Lens App', () => { newTitle: string; } - let defaultArgs: jest.Mocked<{ - editorFrame: EditorFrameInstance; - navigation: typeof navigationStartMock; - data: typeof dataStartMock; - core: typeof core; - storage: Storage; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: ( - id?: string, - returnToOrigin?: boolean, - originatingApp?: string | undefined, - newlyCreated?: boolean - ) => void; - originatingApp: string | undefined; - }>; + let defaultArgs: ReturnType; beforeEach(() => { defaultArgs = makeDefaultArgs(); @@ -486,30 +475,6 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); }); - it('shows a disabled save button when there are no changes to the document', async () => { - const args = defaultArgs; - (args.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'My cool doc', - expression: '', - } as jest.ResolvedValue); - args.editorFrame = frame; - - instance = mount(); - expect(getButton(instance).disableButton).toEqual(true); - - const onChange = frame.mount.mock.calls[0][1].onChange; - - act(() => { - onChange({ - filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, - }); - }); - instance.update(); - expect(getButton(instance).disableButton).toEqual(false); - }); - it('shows a save button that is enabled when the frame has provided its state', async () => { const args = defaultArgs; args.editorFrame = frame; @@ -691,21 +656,7 @@ describe('Lens App', () => { }); describe('query bar state management', () => { - let defaultArgs: jest.Mocked<{ - editorFrame: EditorFrameInstance; - data: typeof dataStartMock; - navigation: typeof navigationStartMock; - core: typeof core; - storage: Storage; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: ( - id?: string, - returnToOrigin?: boolean, - originatingApp?: string | undefined, - newlyCreated?: boolean - ) => void; - }>; + let defaultArgs: ReturnType; beforeEach(() => { defaultArgs = makeDefaultArgs(); @@ -1001,4 +952,159 @@ describe('Lens App', () => { expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); }); + + describe('showing a confirm message when leaving', () => { + let defaultArgs: ReturnType; + let defaultLeave: jest.Mock; + let confirmLeave: jest.Mock; + + beforeEach(() => { + defaultArgs = makeDefaultArgs(); + defaultLeave = jest.fn(); + confirmLeave = jest.fn(); + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'My cool doc', + expression: 'valid expression', + state: { + query: 'kuery', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + } as jest.ResolvedValue); + }); + + it('should not show a confirm message if there is no expression to save', () => { + instance = mount(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(defaultLeave).toHaveBeenCalled(); + expect(confirmLeave).not.toHaveBeenCalled(); + }); + + it('does not confirm if the user is missing save permissions', () => { + const args = defaultArgs; + args.core.application = { + ...args.core.application, + capabilities: { + ...args.core.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, + }, + }; + args.editorFrame = frame; + + instance = mount(); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(defaultLeave).toHaveBeenCalled(); + expect(confirmLeave).not.toHaveBeenCalled(); + }); + + it('should confirm when leaving with an unsaved doc', () => { + defaultArgs.editorFrame = frame; + instance = mount(); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(confirmLeave).toHaveBeenCalled(); + expect(defaultLeave).not.toHaveBeenCalled(); + }); + + it('should confirm when leaving with unsaved changes to an existing doc', async () => { + defaultArgs.editorFrame = frame; + instance = mount(); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(confirmLeave).toHaveBeenCalled(); + expect(defaultLeave).not.toHaveBeenCalled(); + }); + + it('should not confirm when changes are saved', async () => { + defaultArgs.editorFrame = frame; + instance = mount(); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(defaultLeave).toHaveBeenCalled(); + expect(confirmLeave).not.toHaveBeenCalled(); + }); + + it('should confirm when the latest doc is invalid', async () => { + defaultArgs.editorFrame = frame; + instance = mount(); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: null } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(confirmLeave).toHaveBeenCalled(); + expect(defaultLeave).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index a77fbbb597564..ffa59a6fb6bc9 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { AppMountContext, NotificationsStart } from 'kibana/public'; +import { AppMountContext, AppMountParameters, NotificationsStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { @@ -57,6 +57,7 @@ export function App({ redirectTo, originatingAppFromUrl, navigation, + onAppLeave, }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; @@ -72,6 +73,7 @@ export function App({ newlyCreated?: boolean ) => void; originatingAppFromUrl?: string | undefined; + onAppLeave: AppMountParameters['onAppLeave']; }) { const language = storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); @@ -94,6 +96,12 @@ export function App({ const { lastKnownDoc } = state; + const isSaveable = + lastKnownDoc && + lastKnownDoc.expression && + lastKnownDoc.expression.length > 0 && + core.application.capabilities.visualize.save; + useEffect(() => { // Clear app-specific filters when navigating to Lens. Necessary because Lens // can be loaded without a full page refresh @@ -123,7 +131,31 @@ export function App({ filterSubscription.unsubscribe(); timeSubscription.unsubscribe(); }; - }, []); + }, [data.query.filterManager, data.query.timefilter.timefilter]); + + useEffect(() => { + onAppLeave((actions) => { + // Confirm when the user has made any changes to an existing doc + // or when the user has configured something without saving + if ( + core.application.capabilities.visualize.save && + (state.persistedDoc?.expression + ? !_.isEqual(lastKnownDoc?.expression, state.persistedDoc.expression) + : lastKnownDoc?.expression) + ) { + return actions.confirm( + i18n.translate('xpack.lens.app.unsavedWorkMessage', { + defaultMessage: 'Leave Lens with unsaved work?', + }), + i18n.translate('xpack.lens.app.unsavedWorkTitle', { + defaultMessage: 'Unsaved changes', + }) + ); + } else { + return actions.default(); + } + }); + }, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]); // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { @@ -144,7 +176,7 @@ export function App({ : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), }, ]); - }, [state.persistedDoc && state.persistedDoc.title]); + }, [core.application, core.chrome, core.http.basePath, state.persistedDoc]); useEffect(() => { if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) { @@ -187,13 +219,16 @@ export function App({ redirectTo(); }); } - }, [docId]); - - const isSaveable = - lastKnownDoc && - lastKnownDoc.expression && - lastKnownDoc.expression.length > 0 && - core.application.capabilities.visualize.save; + }, [ + core.notifications, + data.indexPatterns, + data.query.filterManager, + docId, + // TODO: These dependencies are changing too often + // docStorage, + // redirectTo, + // state.persistedDoc, + ]); const runSave = ( saveProps: Omit & { @@ -257,7 +292,7 @@ export function App({ core.notifications.toasts.addDanger({ title: e.message, }), - [] + [core.notifications.toasts] ); const { TopNavMenu } = navigation.ui; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 7c875935f6320..032ce8325dca1 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -92,6 +92,7 @@ export async function mountApp( redirectTo(routeProps, id, returnToOrigin, originatingApp, newlyCreated) } originatingAppFromUrl={originatingAppFromUrl} + onAppLeave={params.onAppLeave} /> ); }; From 071f7ef20be94bf8f36007eddc5aa62d42b594df Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 1 Jun 2020 22:57:44 +0100 Subject: [PATCH 35/69] skip flaky suite (#67821) --- x-pack/test/accessibility/apps/search_profiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 8a13940695f9e..138231d3cf025 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const flyout = getService('flyout'); - describe('Accessibility Search Profiler Editor', () => { + // FLAKY: https://github.com/elastic/kibana/issues/67821 + describe.skip('Accessibility Search Profiler Editor', () => { before(async () => { await PageObjects.common.navigateToApp('searchProfiler'); await a11y.testAppSnapshot(); From 571b3de667b1da2281ad49cbfec948869f7df86e Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 1 Jun 2020 15:25:20 -0700 Subject: [PATCH 36/69] [DOCS] Replace docdir attribute with kib-repo-dir (#67907) --- docs/index.asciidoc | 6 +++--- docs/setup/settings.asciidoc | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 5474772ab7da8..add91600a34ea 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -3,11 +3,11 @@ :include-xpack: true :lang: en -:kib-repo-dir: {docdir} +:kib-repo-dir: {kibana-root}/docs :blog-ref: https://www.elastic.co/blog/ :wikipedia: https://en.wikipedia.org/wiki -include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] +include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-repo: docker.elastic.co/kibana/kibana :docker-image: docker.elastic.co/kibana/kibana:{version} @@ -18,7 +18,7 @@ include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] :blob: {repo}blob/{branch}/ :security-ref: https://www.elastic.co/community/security/ -include::{asciidoc-dir}/../../shared/attributes.asciidoc[] +include::{docs-root}/shared/attributes.asciidoc[] include::user/index.asciidoc[] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 6596f93a88f51..42d616c80119b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -627,17 +627,17 @@ Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* |=== -include::{docdir}/settings/alert-action-settings.asciidoc[] -include::{docdir}/settings/apm-settings.asciidoc[] -include::{docdir}/settings/dev-settings.asciidoc[] -include::{docdir}/settings/graph-settings.asciidoc[] -include::{docdir}/settings/infrastructure-ui-settings.asciidoc[] -include::{docdir}/settings/i18n-settings.asciidoc[] -include::{docdir}/settings/logs-ui-settings.asciidoc[] -include::{docdir}/settings/ml-settings.asciidoc[] -include::{docdir}/settings/monitoring-settings.asciidoc[] -include::{docdir}/settings/reporting-settings.asciidoc[] +include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] +include::{kib-repo-dir}/settings/apm-settings.asciidoc[] +include::{kib-repo-dir}/settings/dev-settings.asciidoc[] +include::{kib-repo-dir}/settings/graph-settings.asciidoc[] +include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] +include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] +include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] +include::{kib-repo-dir}/settings/ml-settings.asciidoc[] +include::{kib-repo-dir}/settings/monitoring-settings.asciidoc[] +include::{kib-repo-dir}/settings/reporting-settings.asciidoc[] include::secure-settings.asciidoc[] -include::{docdir}/settings/security-settings.asciidoc[] -include::{docdir}/settings/spaces-settings.asciidoc[] -include::{docdir}/settings/telemetry-settings.asciidoc[] +include::{kib-repo-dir}/settings/security-settings.asciidoc[] +include::{kib-repo-dir}/settings/spaces-settings.asciidoc[] +include::{kib-repo-dir}/settings/telemetry-settings.asciidoc[] From 0dca28b6dd0c6a3c5ef51e5c9578a5f1809c86a8 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 1 Jun 2020 16:30:28 -0600 Subject: [PATCH 37/69] [SEIM][Detection Engine] Moves the io-ts schemas to the common folder from the server side ## Summary This moves the io-ts schemas from the common folder from the server side up to the common folder. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../schemas/common}/schemas.ts | 0 .../schemas/response/__mocks__/utils.ts | 0 .../schemas/response/error_schema.test.ts | 13 +- .../schemas/response/error_schema.ts | 2 +- .../response/find_rules_schema.test.ts | 13 +- .../schemas/response/find_rules_schema.ts | 2 +- .../response/import_rules_schema.test.ts | 13 +- .../schemas/response/import_rules_schema.ts | 2 +- .../response/prepackaged_rules_schema.test.ts | 13 +- .../response/prepackaged_rules_schema.ts | 2 +- .../prepackaged_rules_status_schema.test.ts | 13 +- .../prepackaged_rules_status_schema.ts | 2 +- .../response/rules_bulk_schema.test.ts | 13 +- .../schemas/response/rules_bulk_schema.ts | 0 .../schemas/response/rules_schema.test.ts} | 201 +++++++++++- .../schemas/response/rules_schema.ts | 110 +++++-- .../type_timeline_only_schema.test.ts | 13 +- .../response/type_timeline_only_schema.ts | 2 +- .../schemas/types/iso_date_string.test.ts | 2 +- .../schemas/types/iso_date_string.ts | 0 .../schemas/types/lists_default_array.test.ts | 2 +- .../schemas/types/lists_default_array.ts | 2 +- .../schemas/types/positive_integer.ts | 0 ...positive_integer_greater_than_zero.test.ts | 2 +- .../positive_integer_greater_than_zero.ts | 0 .../schemas/types/postive_integer.test.ts | 2 +- .../types/references_default_array.test.ts | 2 +- .../schemas/types/references_default_array.ts | 0 .../schemas/types/risk_score.test.ts | 2 +- .../schemas/types/risk_score.ts | 0 .../schemas/types/uuid.test.ts | 2 +- .../detection_engine}/schemas/types/uuid.ts | 0 .../rules/add_prepackaged_rules_route.ts | 8 +- .../routes/rules/create_rules_bulk_route.ts | 2 +- .../routes/rules/delete_rules_bulk_route.ts | 2 +- .../get_prepackaged_rules_status_route.ts | 8 +- .../routes/rules/import_rules_route.ts | 5 +- .../routes/rules/patch_rules_bulk_route.ts | 2 +- .../routes/rules/update_rules_bulk_route.ts | 2 +- .../routes/rules/validate.test.ts | 2 +- .../detection_engine/routes/rules/validate.ts | 7 +- .../schemas/response/check_type_dependents.ts | 97 ------ .../schemas/response/rules_schema.test.ts | 289 ------------------ .../signals/build_exceptions_query.test.ts | 2 +- .../signals/build_exceptions_query.ts | 6 +- .../signals/filter_events_with_list.ts | 2 +- .../siem/server/lib/detection_engine/types.ts | 2 +- .../timeline/routes/import_timelines_route.ts | 2 +- 48 files changed, 333 insertions(+), 535 deletions(-) rename x-pack/plugins/siem/{server/lib/detection_engine/routes/schemas/response => common/detection_engine/schemas/common}/schemas.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/__mocks__/utils.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/error_schema.test.ts (86%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/error_schema.ts (92%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/find_rules_schema.test.ts (93%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/find_rules_schema.ts (89%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/import_rules_schema.test.ts (92%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/import_rules_schema.ts (91%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/prepackaged_rules_schema.test.ts (90%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/prepackaged_rules_schema.ts (89%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/prepackaged_rules_status_schema.test.ts (92%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/prepackaged_rules_status_schema.ts (96%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/rules_bulk_schema.test.ts (93%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/rules_bulk_schema.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts => common/detection_engine/schemas/response/rules_schema.test.ts} (70%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/rules_schema.ts (54%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/type_timeline_only_schema.test.ts (85%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/response/type_timeline_only_schema.ts (92%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/iso_date_string.test.ts (95%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/iso_date_string.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/lists_default_array.test.ts (98%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/lists_default_array.ts (97%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/positive_integer.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/positive_integer_greater_than_zero.test.ts (95%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/positive_integer_greater_than_zero.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/postive_integer.test.ts (95%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/references_default_array.test.ts (95%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/references_default_array.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/risk_score.test.ts (96%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/risk_score.ts (100%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/uuid.test.ts (94%) rename x-pack/plugins/siem/{server/lib/detection_engine/routes => common/detection_engine}/schemas/types/uuid.ts (100%) delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/__mocks__/utils.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/__mocks__/utils.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts similarity index 86% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts index 9bbde3d5236db..2a4d75522d010 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts @@ -9,19 +9,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { getErrorPayload } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('error_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an error with a UUID given for id', () => { const error = getErrorPayload(); const decoded = errorSchema.decode(getErrorPayload()); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts index f9c776e3b3cdc..986d3ad87ec85 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { rule_id, status_code, message } from './schemas'; +import { rule_id, status_code, message } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ // We use id: t.string intentionally and _never_ the id from global schemas as diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts similarity index 93% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts index 1b7d7994462c7..51163c3d76ed6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts @@ -9,19 +9,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { getFindResponseSingle, getBaseResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; -import { exactCheck } from '../../../../../../common/exact_check'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('find_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate a typical single find rules response', () => { const payload = getFindResponseSingle(); const decoded = findRulesSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts similarity index 89% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts index d7e8a246cfe01..77077ce2e22ac 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { rulesSchema } from './rules_schema'; -import { page, perPage, total } from './schemas'; +import { page, perPage, total } from '../common/schemas'; export const findRulesSchema = t.exact( t.type({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts index 18e17a319883a..d7efe4b30af11 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts @@ -8,20 +8,11 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left, Either } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; import { Errors } from 'io-ts'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('import_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an empty import response with no errors', () => { const payload: ImportRulesSchema = { success: true, success_count: 0, errors: [] }; const decoded = importRulesSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts similarity index 91% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts index dec32b18e2b24..adea77e7b933f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { success, success_count } from './schemas'; +import { success, success_count } from '../common/schemas'; import { errorSchema } from './error_schema'; /* eslint-enable @typescript-eslint/camelcase */ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts similarity index 90% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts index 2d3fd75914822..fc3f89996daf1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts @@ -7,19 +7,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; const decoded = prePackagedRulesSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts similarity index 89% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts index f0eff0ba19753..3b0107c91fee0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { rules_installed, rules_updated } from './schemas'; +import { rules_installed, rules_updated } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ export const prePackagedRulesSchema = t.exact( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts index abe601a546111..eeae72209829e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts @@ -10,19 +10,10 @@ import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, } from './prepackaged_rules_status_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesStatusSchema = { rules_installed: 0, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts similarity index 96% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts index 72e5821eb4697..ee8e7b48a58bc 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts @@ -12,7 +12,7 @@ import { rules_custom_installed, rules_not_installed, rules_not_updated, -} from './schemas'; +} from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ export const prePackagedRulesStatusSchema = t.exact( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts similarity index 93% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts index 98cb2ef058485..04cf012f36dba 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts @@ -11,19 +11,10 @@ import { getBaseResponsePayload, getErrorPayload } from './__mocks__/utils'; import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rule_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate a regular message and and error together with a uuid', () => { const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload()]; const decoded = rulesBulkSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts similarity index 70% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts index 0b0d3bf43b1e9..8ed9c30507f4f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts @@ -4,32 +4,209 @@ * you may not use this file except in compliance with the Elastic License. */ +import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { + rulesSchema, + RulesSchema, checkTypeDependents, getDependents, addSavedId, - addTimelineTitle, addQueryFields, + addTimelineTitle, addMlFields, -} from './check_type_dependents'; +} from './rules_schema'; import { getBaseResponsePayload, getMlRuleResponsePayload } from './__mocks__/utils'; -import { left } from 'fp-ts/lib/Either'; -import { RulesSchema } from './rules_schema'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; -describe('check_type_dependents', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); +export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; + +describe('rules_schema', () => { + test('it should validate a type of "query" without anything extra', () => { + const payload = getBaseResponsePayload(); + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate invalid_data for the type', () => { + const payload: Omit & { type: string } = getBaseResponsePayload(); + payload.type = 'invalid_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" with a saved_id together', () => { + const payload = getBaseResponsePayload(); + payload.type = 'query'; + payload.saved_id = 'save id 123'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expected.type = 'saved_query'; + expected.saved_id = 'save id 123'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + delete payload.saved_id; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "saved_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + expected.timeline_id = 'some timeline id'; + expected.timeline_title = 'some timeline title'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); }); - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); + test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_id = 'some timeline id'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); }); describe('checkTypeDependents', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts similarity index 54% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts index fb1ee8e670e31..a7a31ec9e1b59 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts @@ -7,10 +7,11 @@ /* eslint-disable @typescript-eslint/camelcase */ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; -import { Either, fold, right, left } from 'fp-ts/lib/Either'; - +import { Either, left, fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { checkTypeDependents } from './check_type_dependents'; +import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; +import { isMlRule } from '../../../machine_learning/helpers'; + import { actions, anomaly_threshold, @@ -54,9 +55,8 @@ import { filters, meta, note, -} from './schemas'; +} from '../common/schemas'; import { ListsDefaultArray } from '../types/lists_default_array'; -import { hasListsFeature } from '../../../feature_flags'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -155,32 +155,92 @@ export const rulesSchema = new t.Type< 'RulesSchema', (input: unknown): input is RulesWithoutTypeDependentsSchema => isObject(input), (input): Either => { - const output = checkTypeDependents(input); - if (!hasListsFeature()) { - // TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release - return removeList(output); - } else { - return output; - } + return checkTypeDependents(input); }, t.identity ); -// TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release -export const removeList = ( - decoded: Either -): Either => { - const onLeft = (errors: t.Errors): Either => left(errors); - const onRight = (decodedValue: RequiredRulesSchema): Either => { - delete decodedValue.exceptions_list; - return right(decodedValue); - }; - const folded = fold(onLeft, onRight); - return pipe(decoded, folded); -}; - /** * This is the correct type you want to use for Rules that are outputted from the * REST interface. This has all base and all optional properties merged together. */ export type RulesSchema = t.TypeOf; + +export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'saved_query') { + return [t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id }))]; + } else { + return []; + } +}; + +export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.timeline_id != null) { + return [ + t.exact(t.type({ timeline_title: dependentRulesSchema.props.timeline_title })), + t.exact(t.type({ timeline_id: dependentRulesSchema.props.timeline_id })), + ]; + } else { + return []; + } +}; + +export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { + return [ + t.exact(t.type({ query: dependentRulesSchema.props.query })), + t.exact(t.type({ language: dependentRulesSchema.props.language })), + ]; + } else { + return []; + } +}; + +export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (isMlRule(typeAndTimelineOnly.type)) { + return [ + t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), + t.exact( + t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) + ), + ]; + } else { + return []; + } +}; + +export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { + const dependents: t.Mixed[] = [ + t.exact(requiredRulesSchema), + t.exact(partialRulesSchema), + ...addSavedId(typeAndTimelineOnly), + ...addTimelineTitle(typeAndTimelineOnly), + ...addQueryFields(typeAndTimelineOnly), + ...addMlFields(typeAndTimelineOnly), + ]; + + if (dependents.length > 1) { + // This unsafe cast is because t.intersection does not use an array but rather a set of + // tuples and really does not look like they expected us to ever dynamically build up + // intersections, but here we are doing that. Looking at their code, although they limit + // the array elements to 5, it looks like you have N number of intersections + const unsafeCast: [t.Mixed, t.Mixed] = dependents as [t.Mixed, t.Mixed]; + return t.intersection(unsafeCast); + } else { + // We are not allowed to call t.intersection with a single value so we return without + // it here normally. + return dependents[0]; + } +}; + +export const checkTypeDependents = (input: unknown): Either => { + const typeOnlyDecoded = typeAndTimelineOnlySchema.decode(input); + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = ( + typeAndTimelineOnly: TypeAndTimelineOnly + ): Either => { + const intersections = getDependents(typeAndTimelineOnly); + return intersections.decode(input); + }; + return pipe(typeOnlyDecoded, fold(onLeft, onRight)); +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts similarity index 85% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts index 8f06e2c6e49b0..c7335ffd62f02 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts @@ -8,19 +8,10 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rule_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate a a type and timeline_id together', () => { const payload: TypeAndTimelineOnly = { type: 'query', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts index 6d11ff03563d1..d23d4ad2e83d4 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { timeline_id, type } from './schemas'; +import { timeline_id, type } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ /** diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts index 9f9181359d44a..e8bce3f38f4b3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts @@ -7,7 +7,7 @@ import { IsoDateString } from './iso_date_string'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts similarity index 98% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts index dc0bd6cacf0d6..31e0a8e5c2c73 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts @@ -7,7 +7,7 @@ import { ListsDefaultArray } from './lists_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('lists_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts similarity index 97% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts index 743914ad070a2..8244f4a29e193 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts @@ -11,7 +11,7 @@ import { list_and as listAnd, list_values as listValues, list_values_operator as listOperator, -} from '../response/schemas'; +} from '../common/schemas'; export type ListsDefaultArrayC = t.Type; export type List = t.TypeOf; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts index a3338c878bd71..821eb066a6531 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts @@ -7,7 +7,7 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts index 48ea2025b9b12..ea00ecf5efe0d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts @@ -7,7 +7,7 @@ import { PositiveInteger } from './positive_integer'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts index 3aaff7e00ad51..43e2dbdac1fe1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts @@ -7,7 +7,7 @@ import { ReferencesDefaultArray } from './references_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('references_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts similarity index 96% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts index 41c0faf4d608d..cf849f28a0963 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts @@ -7,7 +7,7 @@ import { RiskScore } from './risk_score'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('risk_score', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts similarity index 94% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts index b640b449e6b8a..d3a68a7575487 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts @@ -7,7 +7,7 @@ import { UUID } from './uuid'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 83dd87002e8f9..6268451042da1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + PrePackagedRulesSchema, + prePackagedRulesSchema, +} from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { getIndexExists } from '../../index/get_index_exists'; @@ -14,10 +18,6 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { - PrePackagedRulesSchema, - prePackagedRulesSchema, -} from '../schemas/response/prepackaged_rules_schema'; import { validate } from './validate'; export const addPrepackedRulesRoute = (router: IRouter) => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index ff6d212deb584..d88cc7fcde504 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -6,6 +6,7 @@ import uuid from 'uuid'; +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -24,7 +25,6 @@ import { buildSiemResponse, } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 036e29aa0ebe7..01ad3c7d4e726 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter, RouteConfig, RequestHandler } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 67a54f3ba492a..90380b0483c82 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + PrePackagedRulesStatusSchema, + prePackagedRulesStatusSchema, +} from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_status_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; @@ -12,10 +16,6 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { - PrePackagedRulesStatusSchema, - prePackagedRulesStatusSchema, -} from '../schemas/response/prepackaged_rules_status_schema'; import { validate } from './validate'; export const getPrepackagedRulesStatusRoute = (router: IRouter) => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 901e7403bb953..311149087cc49 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -7,6 +7,10 @@ import { chunk } from 'lodash/fp'; import { extname } from 'path'; +import { + ImportRulesSchema, + importRulesSchema, +} from '../../../../../common/detection_engine/schemas/response/import_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -31,7 +35,6 @@ import { import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; -import { ImportRulesSchema, importRulesSchema } from '../schemas/response/import_rules_schema'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { validate } from './validate'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index cc4d5e03500a4..0d0cd28738c92 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -14,7 +15,6 @@ import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../ import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index c0dfecc71ce05..335684dc38b32 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -15,7 +16,6 @@ import { transformValidateBulkError, validate } from './validate'; import { buildRouteValidation, transformBulkError, buildSiemResponse } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { updateRules } from '../../rules/update_rules'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 9069202d4d3aa..13a5bbd2afc0c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -14,9 +14,9 @@ import { } from './validate'; import { getResult } from '../__mocks__/request_responses'; import { FindResult } from '../../../../../../alerting/server'; -import { RulesSchema } from '../schemas/response/rules_schema'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; export const ruleOutput: RulesSchema = { actions: [], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index cda3a4b81ed9b..1220b12d1d1b1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -9,6 +9,11 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema'; +import { + RulesSchema, + rulesSchema, +} from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { formatErrors } from '../../../../../common/format_errors'; import { exactCheck } from '../../../../../common/exact_check'; import { PartialAlert, FindResult } from '../../../../../../alerting/server'; @@ -19,9 +24,7 @@ import { } from '../../rules/types'; import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError } from '../utils'; -import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; -import { findRulesSchema } from '../schemas/response/find_rules_schema'; import { RuleActions } from '../../rule_actions/types'; export const transformValidateFindAlerts = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts deleted file mode 100644 index 1c1bee58f0c97..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; -import { Either, left, fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { isMlRule } from '../../../../../../common/machine_learning/helpers'; -import { - dependentRulesSchema, - RequiredRulesSchema, - partialRulesSchema, - requiredRulesSchema, -} from './rules_schema'; -import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; - -export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'saved_query') { - return [t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id }))]; - } else { - return []; - } -}; - -export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.timeline_id != null) { - return [ - t.exact(t.type({ timeline_title: dependentRulesSchema.props.timeline_title })), - t.exact(t.type({ timeline_id: dependentRulesSchema.props.timeline_id })), - ]; - } else { - return []; - } -}; - -export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { - return [ - t.exact(t.type({ query: dependentRulesSchema.props.query })), - t.exact(t.type({ language: dependentRulesSchema.props.language })), - ]; - } else { - return []; - } -}; - -export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (isMlRule(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), - t.exact( - t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) - ), - ]; - } else { - return []; - } -}; - -export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { - const dependents: t.Mixed[] = [ - t.exact(requiredRulesSchema), - t.exact(partialRulesSchema), - ...addSavedId(typeAndTimelineOnly), - ...addTimelineTitle(typeAndTimelineOnly), - ...addQueryFields(typeAndTimelineOnly), - ...addMlFields(typeAndTimelineOnly), - ]; - - if (dependents.length > 1) { - // This unsafe cast is because t.intersection does not use an array but rather a set of - // tuples and really does not look like they expected us to ever dynamically build up - // intersections, but here we are doing that. Looking at their code, although they limit - // the array elements to 5, it looks like you have N number of intersections - const unsafeCast: [t.Mixed, t.Mixed] = dependents as [t.Mixed, t.Mixed]; - return t.intersection(unsafeCast); - } else { - // We are not allowed to call t.intersection with a single value so we return without - // it here normally. - return dependents[0]; - } -}; - -export const checkTypeDependents = (input: unknown): Either => { - const typeOnlyDecoded = typeAndTimelineOnlySchema.decode(input); - const onLeft = (errors: t.Errors): Either => left(errors); - const onRight = ( - typeAndTimelineOnly: TypeAndTimelineOnly - ): Either => { - const intersections = getDependents(typeAndTimelineOnly); - return intersections.decode(input); - }; - return pipe(typeOnlyDecoded, fold(onLeft, onRight)); -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts deleted file mode 100644 index ade4d12517aca..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { left } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { rulesSchema, RulesSchema, removeList } from './rules_schema'; -import { getBaseResponsePayload } from './__mocks__/utils'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; -import { exactCheck } from '../../../../../../common/exact_check'; - -export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; - -describe('rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('it should validate a type of "query" without anything extra', () => { - const payload = getBaseResponsePayload(); - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getBaseResponsePayload(); - payload.type = 'invalid_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid_data" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getBaseResponsePayload(); - payload.type = 'query'; - payload.saved_id = 'save id 123'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); - - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); - payload.type = 'saved_query'; - delete payload.saved_id; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "saved_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); - expected.timeline_id = 'some timeline id'; - expected.timeline_title = 'some timeline title'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); - payload.timeline_id = 'some timeline id'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getBaseResponsePayload(); - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getBaseResponsePayload(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getBaseResponsePayload(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_id = 'some timeline id'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - // TODO: (LIST-FEATURE) Remove this test once the feature flag is deployed - test('it should remove exceptions_list when we need it to be removed because the feature is off but there exists a list in the data', () => { - const payload = getBaseResponsePayload(); - const decoded = rulesSchema.decode(payload); - const listRemoved = removeList(decoded); - const message = pipe(listRemoved, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: '2020-02-20T03:57:54.037Z', - updated_at: '2020-02-20T03:57:54.037Z', - 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', - updated_by: 'elastic_kibana', - tags: [], - to: 'now', - type: 'query', - threat: [], - version: 1, - output_index: '.siem-signals-hassanabad-frank-default', - max_signals: 100, - risk_score: 55, - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', - }); - }); - - test('it should work with exceptions_list that are not there and not cause invalidation or errors', () => { - const payload = getBaseResponsePayload(); - const { exceptions_list, ...payloadWithoutLists } = payload; - const decoded = rulesSchema.decode(payloadWithoutLists); - const listRemoved = removeList(decoded); - const message = pipe(listRemoved, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: '2020-02-20T03:57:54.037Z', - updated_at: '2020-02-20T03:57:54.037Z', - 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', - updated_by: 'elastic_kibana', - tags: [], - to: 'now', - type: 'query', - threat: [], - version: 1, - output_index: '.siem-signals-hassanabad-frank-default', - max_signals: 100, - risk_score: 55, - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', - }); - }); -}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts index ec8db77dac725..772ebd932698b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -15,7 +15,7 @@ import { formatQuery, getLanguageBooleanOperator, } from './build_exceptions_query'; -import { List } from '../routes/schemas/types/lists_default_array'; +import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; describe('build_exceptions_query', () => { describe('getLanguageBooleanOperator', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts index e7be5025a51f0..b33a2376589ef 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -3,8 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { + ListOperator, + ListValues, + List, +} from '../../../../common/detection_engine/schemas/types/lists_default_array'; import { Query } from '../../../../../../../src/plugins/data/server'; -import { List, ListOperator, ListValues } from '../routes/schemas/types/lists_default_array'; import { RuleAlertParams, Language } from '../types'; type Operators = 'and' | 'or' | 'not'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts index 400bb5dda46e7..07435fda0da2e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -6,11 +6,11 @@ import { get } from 'lodash/fp'; import { Logger } from 'src/core/server'; +import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; import { type } from '../../../../../lists/common/schemas/common'; import { ListClient } from '../../../../../lists/server'; import { SignalSearchResponse, SearchTypes } from './types'; import { RuleAlertParams } from '../types'; -import { List } from '../routes/schemas/types/lists_default_array'; interface FilterEventsAgainstList { listClient: ListClient; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/types.ts index f2026804da51a..53c8a9bf0a7e7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/types.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ListsDefaultArraySchema } from '../../../common/detection_engine/schemas/types/lists_default_array'; import { CallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; -import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; import { RuleAlertAction, RuleType } from '../../../common/detection_engine/types'; export type PartialFilter = Partial; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 48c6081e855a0..c16b73ff51b56 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -7,6 +7,7 @@ import { extname } from 'path'; import { chunk, omit } from 'lodash/fp'; +import { importRulesSchema } from '../../../../common/detection_engine/schemas/response/import_rules_schema'; import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; import { IRouter } from '../../../../../../../src/core/server'; @@ -16,7 +17,6 @@ import { SetupPlugins } from '../../../plugin'; import { ConfigType } from '../../../config'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; -import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; import { validate } from '../../detection_engine/routes/rules/validate'; import { buildSiemResponse, From 78d5026fbd833ddb8deb0f6bb968f4f82d2559e3 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 1 Jun 2020 20:38:53 -0500 Subject: [PATCH 38/69] APM-specific Jest configuration (#67858) Update the x-pack `createJestConfig` function to take the `rootDir` as an argument, which allows for easier overriding of the Jest configuration for a specific directory. Previously we would run Jest in development from the x-pack directory by running something like: ``` node scripts/jest.js --testPathPattern=plugins/apm --watch ``` Currently (for me anyway) this is failing with: ``` Error: EMFILE: too many open files, watch at FSEvent.FSWatcher._handle.onchange (internal/fs/watchers.js:123:28) ``` and it would sometimes not correctly test only the changed files when a change in APM was made. It was also difficult to configure correctly with the [VSCode Jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest). Add a jest.config.js for APM. This makes running with `--watch` better about which files it chooses to re-run and makes the VSCode extension work (including coverage mapping) with minimal configuration. --- x-pack/dev-tools/jest/create_jest_config.js | 12 +++--- x-pack/dev-tools/jest/index.js | 1 + x-pack/plugins/apm/dev_docs/vscode_setup.md | 28 +++++-------- x-pack/plugins/apm/jest.config.js | 44 +++++++++++++++++++++ x-pack/plugins/apm/readme.md | 14 +++++-- 5 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/apm/jest.config.js diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 3d8b45e7d1b83..a222e11d28f4a 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { +export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirectory }) { const fileMockPath = `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`; return { - rootDir: xPackKibanaDirectory, + rootDir, roots: ['/plugins', '/legacy/plugins', '/legacy/server'], moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], moduleNameMapper: { @@ -44,15 +44,15 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '!**/plugins/apm/e2e/**', ], coveragePathIgnorePatterns: ['.*\\.d\\.ts'], - coverageDirectory: '/../target/kibana-coverage/jest', + coverageDirectory: `${kibanaDirectory}/target/kibana-coverage/jest`, coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ `${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`, - `/dev-tools/jest/setup/polyfills.js`, - `/dev-tools/jest/setup/enzyme.js`, + `${xPackKibanaDirectory}/dev-tools/jest/setup/polyfills.js`, + `${xPackKibanaDirectory}/dev-tools/jest/setup/enzyme.js`, ], setupFilesAfterEnv: [ - `/dev-tools/jest/setup/setup_test.js`, + `${xPackKibanaDirectory}/dev-tools/jest/setup/setup_test.js`, `${kibanaDirectory}/src/dev/jest/setup/mocks.js`, `${kibanaDirectory}/src/dev/jest/setup/react_testing_library.js`, ], diff --git a/x-pack/dev-tools/jest/index.js b/x-pack/dev-tools/jest/index.js index f61c50f989503..2f831e33cdd11 100644 --- a/x-pack/dev-tools/jest/index.js +++ b/x-pack/dev-tools/jest/index.js @@ -14,6 +14,7 @@ export function runJest() { const config = JSON.stringify( createJestConfig({ kibanaDirectory: resolve(__dirname, '../../..'), + rootDir: resolve(__dirname, '../..'), xPackKibanaDirectory: resolve(__dirname, '../..'), }) ); diff --git a/x-pack/plugins/apm/dev_docs/vscode_setup.md b/x-pack/plugins/apm/dev_docs/vscode_setup.md index 1c80d1476520d..c7adad4fd0942 100644 --- a/x-pack/plugins/apm/dev_docs/vscode_setup.md +++ b/x-pack/plugins/apm/dev_docs/vscode_setup.md @@ -1,8 +1,8 @@ -### Visual Studio Code +# Visual Studio Code When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. -#### Using the Jest extension +## Using the Jest extension The [vscode-jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) is a good way to run your Jest tests inside the editor. @@ -22,31 +22,21 @@ If you have a workspace configured as described above you should have: "jest.disabledWorkspaceFolders": ["kibana", "x-pack"] ``` -in your Workspace settings, and: - -```json -"jest.pathToJest": "node scripts/jest.js --testPathPattern=plugins/apm", -"jest.rootPath": "../../.." -``` - -in the settings for the APM folder. - -#### Jest debugging +## Jest debugging To make the [VSCode debugger](https://vscode.readthedocs.io/en/latest/editor/debugging/) work with Jest (you can set breakpoints in the code and tests and use the VSCode debugger) you'll need the [Node Debug extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.node-debug2) installed and can set up a launch configuration like: ```json { "type": "node", - "name": "APM Jest", + "name": "vscode-jest-tests", "request": "launch", - "args": ["--runInBand", "--testPathPattern=plugins/apm"], - "cwd": "${workspaceFolder}/../../..", - "console": "internalConsole", - "internalConsoleOptions": "openOnSessionStart", + "args": ["--runInBand"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true, - "program": "${workspaceFolder}/../../../scripts/jest.js", - "runtimeVersion": "10.15.2" + "program": "${workspaceFolder}/../../../node_modules/jest/bin/jest" } ``` diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js new file mode 100644 index 0000000000000..c3ae694fe8e14 --- /dev/null +++ b/x-pack/plugins/apm/jest.config.js @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This is an APM-specific Jest configuration which overrides the x-pack +// configuration. It's intended for use in development and does not run in CI, +// which runs the entire x-pack suite. Run `npx jest`. + +require('../../../src/setup_node_env'); + +const { createJestConfig } = require('../../dev-tools/jest/create_jest_config'); +const { resolve } = require('path'); + +const rootDir = resolve(__dirname, '.'); +const xPackKibanaDirectory = resolve(__dirname, '../..'); +const kibanaDirectory = resolve(__dirname, '../../..'); + +const jestConfig = createJestConfig({ + kibanaDirectory, + rootDir, + xPackKibanaDirectory, +}); + +module.exports = { + ...jestConfig, + reporters: ['default'], + roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', + '!**/*.test.{js,ts,tsx}', + '!**/dev_docs/**', + '!**/e2e/**', + '!**/scripts/**', + '!**/target/**', + '!**/typings/**', + '!**/mocks/**', + ], + coverageDirectory: `${rootDir}/target/coverage/jest`, + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index 62465e920d793..ceed5e6c39716 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -39,18 +39,26 @@ _Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sa ### Unit testing -Note: Run the following commands from `kibana/x-pack`. +Note: Run the following commands from `kibana/x-pack/plugins/apm`. #### Run unit tests ``` -node scripts/jest.js plugins/apm --watch +npx jest --watch ``` #### Update snapshots ``` -node scripts/jest.js plugins/apm --updateSnapshot +npx jest --updateSnapshot +``` + +#### Coverage + +HTML coverage report can be found in target/coverage/jest after tests have run. + +``` +open target/coverage/jest/index.html ``` ### Functional tests From ce45dad8b6979b0123d15920e25b4289bc5bdb9b Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 1 Jun 2020 18:45:36 -0700 Subject: [PATCH 39/69] Changed alerting API endpoints urls, bodies and params to follow Kibana STYLEGUIDE (#66838) * Changed alerting API endpoints urls, bodies and params to follow Kibana STYLEGUIDE * Changed alerting REST API to keep the pattern 'alerts/alert/{id}' * fixed tests * fixed tests * Fixed jest tests * Renamed plugin from alerting to alerts * fixed tests * fixed tests * Fixed alert type check error * Fixed find api * fixed type checks * fixed tests security issues * Fixed view in app * - Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 2 +- examples/alerting_example/kibana.json | 2 +- .../public/alert_types/astros.tsx | 8 +- .../public/alert_types/index.ts | 10 +-- .../public/components/view_alert.tsx | 2 +- .../public/components/view_astros_alert.tsx | 2 +- examples/alerting_example/public/plugin.tsx | 10 +-- .../server/alert_types/always_firing.ts | 2 +- .../server/alert_types/astros.ts | 2 +- examples/alerting_example/server/plugin.ts | 10 +-- rfcs/text/0003_handler_interface.md | 2 +- x-pack/.i18nrc.json | 2 +- x-pack/legacy/plugins/monitoring/index.ts | 2 +- x-pack/plugins/actions/README.md | 4 +- x-pack/plugins/alerting_builtins/README.md | 2 +- x-pack/plugins/alerting_builtins/kibana.json | 2 +- .../server/alert_types/index.ts | 2 +- .../index_threshold/action_context.ts | 2 +- .../alert_types/index_threshold/index.ts | 6 +- .../index_threshold/lib/date_range_info.ts | 2 +- .../index_threshold/lib/time_series_types.ts | 2 +- .../alerting_builtins/server/plugin.test.ts | 6 +- .../alerting_builtins/server/plugin.ts | 4 +- .../plugins/alerting_builtins/server/types.ts | 6 +- x-pack/plugins/{alerting => alerts}/README.md | 62 +++++++-------- .../{alerting => alerts}/common/alert.ts | 0 .../common/alert_instance.ts | 0 .../common/alert_navigation.ts | 0 .../common/alert_task_instance.ts | 0 .../{alerting => alerts}/common/alert_type.ts | 0 .../common/date_from_string.test.ts | 0 .../common/date_from_string.ts | 0 .../{alerting => alerts}/common/index.ts | 2 +- .../common/parse_duration.test.ts | 0 .../common/parse_duration.ts | 0 .../plugins/{alerting => alerts}/kibana.json | 4 +- .../public/alert_api.test.ts | 12 +-- .../{alerting => alerts}/public/alert_api.ts | 10 +-- .../alert_navigation_registry.mock.ts | 0 .../alert_navigation_registry.test.ts | 0 .../alert_navigation_registry.ts | 6 +- .../public/alert_navigation_registry/index.ts | 0 .../public/alert_navigation_registry/types.ts | 0 .../{alerting => alerts}/public/index.ts | 0 .../{alerting => alerts}/public/mocks.ts | 0 .../{alerting => alerts}/public/plugin.ts | 0 .../alert_instance/alert_instance.test.ts | 0 .../server/alert_instance/alert_instance.ts | 0 .../create_alert_instance_factory.test.ts | 0 .../create_alert_instance_factory.ts | 0 .../server/alert_instance/index.ts | 0 .../server/alert_type_registry.mock.ts | 0 .../server/alert_type_registry.test.ts | 2 +- .../server/alert_type_registry.ts | 6 +- .../server/alerts_client.mock.ts | 0 .../server/alerts_client.test.ts | 8 +- .../server/alerts_client.ts | 55 +++++++------- .../server/alerts_client_factory.test.ts | 8 +- .../server/alerts_client_factory.ts | 6 +- .../server/constants/plugin.ts | 6 +- .../{alerting => alerts}/server/index.ts | 0 .../lib/delete_task_if_it_exists.test.ts | 0 .../server/lib/delete_task_if_it_exists.ts | 0 .../{alerting => alerts}/server/lib/index.ts | 0 .../lib/is_alert_not_found_error.test.ts | 0 .../server/lib/is_alert_not_found_error.ts | 0 .../server/lib/license_api_access.ts | 0 .../server/lib/license_state.mock.ts | 0 .../server/lib/license_state.test.ts | 2 +- .../server/lib/license_state.ts | 6 +- .../server/lib/result_type.ts | 0 .../server/lib/types.test.ts | 0 .../{alerting => alerts}/server/lib/types.ts | 0 .../lib/validate_alert_type_params.test.ts | 0 .../server/lib/validate_alert_type_params.ts | 0 .../{alerting => alerts}/server/mocks.ts | 0 .../server/plugin.test.ts | 4 +- .../{alerting => alerts}/server/plugin.ts | 2 +- .../server/routes/_mock_handler_arguments.ts | 0 .../server/routes/create.test.ts | 2 +- .../server/routes/create.ts | 2 +- .../server/routes/delete.test.ts | 2 +- .../server/routes/delete.ts | 2 +- .../server/routes/disable.test.ts | 2 +- .../server/routes/disable.ts | 2 +- .../server/routes/enable.test.ts | 2 +- .../server/routes/enable.ts | 2 +- .../server/routes/find.test.ts | 7 +- .../server/routes/find.ts | 33 ++++---- .../server/routes/get.test.ts | 2 +- .../{alerting => alerts}/server/routes/get.ts | 2 +- .../server/routes/get_alert_state.test.ts | 6 +- .../server/routes/get_alert_state.ts | 2 +- .../server/routes/health.test.ts | 2 +- .../server/routes/health.ts | 2 +- .../server/routes/index.ts | 0 .../server/routes/lib/error_handler.ts | 2 +- .../alerts/server/routes/lib/rename_keys.ts | 16 ++++ .../server/routes/list_alert_types.test.ts | 6 +- .../server/routes/list_alert_types.ts | 2 +- .../server/routes/mute_all.test.ts | 2 +- .../server/routes/mute_all.ts | 2 +- .../server/routes/mute_instance.test.ts | 6 +- .../server/routes/mute_instance.ts | 18 +++-- .../server/routes/unmute_all.test.ts | 2 +- .../server/routes/unmute_all.ts | 2 +- .../server/routes/unmute_instance.test.ts | 2 +- .../server/routes/unmute_instance.ts | 2 +- .../server/routes/update.test.ts | 2 +- .../server/routes/update.ts | 2 +- .../server/routes/update_api_key.test.ts | 2 +- .../server/routes/update_api_key.ts | 2 +- .../server/saved_objects/index.ts | 0 .../server/saved_objects/mappings.json | 0 .../task_runner/alert_task_instance.test.ts | 2 +- .../server/task_runner/alert_task_instance.ts | 2 +- .../create_execution_handler.test.ts | 0 .../task_runner/create_execution_handler.ts | 2 +- .../task_runner/get_next_run_at.test.ts | 0 .../server/task_runner/get_next_run_at.ts | 0 .../server/task_runner/index.ts | 0 .../server/task_runner/task_runner.test.ts | 4 +- .../server/task_runner/task_runner.ts | 2 +- .../task_runner/task_runner_factory.test.ts | 4 +- .../server/task_runner/task_runner_factory.ts | 6 +- .../transform_action_params.test.ts | 0 .../task_runner/transform_action_params.ts | 0 .../server/test_utils/index.ts | 0 .../{alerting => alerts}/server/types.ts | 0 .../server/usage/alerts_telemetry.test.ts | 0 .../server/usage/alerts_telemetry.ts | 0 .../usage/alerts_usage_collector.test.ts | 0 .../server/usage/alerts_usage_collector.ts | 0 .../server/usage/index.ts | 0 .../{alerting => alerts}/server/usage/task.ts | 0 .../server/usage/types.ts | 0 x-pack/plugins/apm/kibana.json | 2 +- .../components/app/ServiceDetails/index.tsx | 2 +- x-pack/plugins/apm/public/plugin.ts | 6 +- .../server/lib/alerts/register_apm_alerts.ts | 8 +- .../alerts/register_error_rate_alert_type.ts | 8 +- ...egister_transaction_duration_alert_type.ts | 8 +- x-pack/plugins/apm/server/plugin.ts | 8 +- x-pack/plugins/infra/kibana.json | 2 +- .../lib/adapters/framework/adapter_types.ts | 4 +- .../inventory_metric_threshold_executor.ts | 2 +- .../log_threshold_executor.test.ts | 4 +- .../log_threshold/log_threshold_executor.ts | 2 +- .../register_log_threshold_alert_type.ts | 2 +- .../metric_threshold_executor.test.ts | 4 +- .../metric_threshold_executor.ts | 2 +- .../lib/alerting/register_alert_types.ts | 2 +- x-pack/plugins/infra/server/plugin.ts | 2 +- x-pack/plugins/monitoring/kibana.json | 2 +- .../public/components/alerts/status.tsx | 2 +- .../server/alerts/cluster_state.test.ts | 2 +- .../monitoring/server/alerts/cluster_state.ts | 2 +- .../server/alerts/license_expiration.test.ts | 2 +- .../server/alerts/license_expiration.ts | 2 +- .../monitoring/server/alerts/types.d.ts | 2 +- .../server/lib/alerts/cluster_state.lib.ts | 2 +- .../server/lib/alerts/fetch_status.ts | 2 +- .../server/lib/alerts/get_prepared_alert.ts | 2 +- .../lib/alerts/license_expiration.lib.ts | 2 +- x-pack/plugins/monitoring/server/plugin.ts | 12 +-- .../schemas/common/schemas.ts | 2 +- .../detection_engine/transform_actions.ts | 2 +- .../siem/common/detection_engine/types.ts | 2 +- x-pack/plugins/siem/kibana.json | 2 +- .../description_step/actions_description.tsx | 2 +- .../rules/rule_actions_field/index.tsx | 2 +- .../detection_engine/rules/types.ts | 2 +- .../pages/detection_engine/rules/types.ts | 2 +- .../server/lib/detection_engine/README.md | 2 +- .../create_notifications.test.ts | 2 +- .../notifications/create_notifications.ts | 2 +- .../delete_notifications.test.ts | 2 +- .../notifications/find_notifications.ts | 2 +- .../notifications/get_signals_count.ts | 2 +- .../notifications/read_notifications.test.ts | 2 +- .../notifications/read_notifications.ts | 2 +- .../rules_notification_alert_type.test.ts | 2 +- .../schedule_notification_actions.ts | 2 +- .../detection_engine/notifications/types.ts | 4 +- .../update_notifications.test.ts | 2 +- .../notifications/update_notifications.ts | 2 +- .../routes/__mocks__/request_context.ts | 2 +- .../routes/rules/utils.test.ts | 4 +- .../detection_engine/routes/rules/utils.ts | 2 +- .../routes/rules/validate.test.ts | 2 +- .../detection_engine/routes/rules/validate.ts | 2 +- .../add_prepackaged_rules_schema.test.ts | 2 +- .../schemas/create_rules_schema.test.ts | 2 +- .../schemas/import_rules_schema.test.ts | 2 +- .../routes/schemas/patch_rules_schema.test.ts | 2 +- .../schemas/update_rules_schema.test.ts | 2 +- .../create_rule_actions_saved_object.ts | 2 +- .../delete_rule_actions_saved_object.ts | 2 +- .../get_rule_actions_saved_object.ts | 2 +- ...ate_or_create_rule_actions_saved_object.ts | 2 +- .../update_rule_actions_saved_object.ts | 2 +- .../rules/create_rules.test.ts | 2 +- .../detection_engine/rules/create_rules.ts | 2 +- .../rules/delete_rules.test.ts | 2 +- .../lib/detection_engine/rules/find_rules.ts | 2 +- .../get_existing_prepackaged_rules.test.ts | 2 +- .../rules/get_existing_prepackaged_rules.ts | 2 +- .../rules/get_export_all.test.ts | 2 +- .../detection_engine/rules/get_export_all.ts | 2 +- .../rules/get_export_by_object_ids.test.ts | 2 +- .../rules/get_export_by_object_ids.ts | 2 +- .../rules/install_prepacked_rules.ts | 4 +- .../rules/patch_rules.test.ts | 2 +- .../lib/detection_engine/rules/patch_rules.ts | 2 +- .../detection_engine/rules/read_rules.test.ts | 2 +- .../lib/detection_engine/rules/read_rules.ts | 2 +- .../lib/detection_engine/rules/types.ts | 4 +- .../rules/update_prepacked_rules.test.ts | 2 +- .../rules/update_prepacked_rules.ts | 2 +- .../rules/update_rules.test.ts | 2 +- .../detection_engine/rules/update_rules.ts | 2 +- .../rules/update_rules_notifications.ts | 2 +- .../scripts/get_alert_instances.sh | 4 +- .../scripts/get_alert_types.sh | 4 +- .../signals/bulk_create_ml_signals.ts | 2 +- .../signals/get_filter.test.ts | 2 +- .../detection_engine/signals/get_filter.ts | 2 +- .../signals/get_input_output_index.test.ts | 2 +- .../signals/get_input_output_index.ts | 2 +- .../signals/search_after_bulk_create.test.ts | 2 +- .../signals/search_after_bulk_create.ts | 2 +- .../signals/signal_rule_alert_type.test.ts | 2 +- .../signals/single_bulk_create.test.ts | 2 +- .../signals/single_bulk_create.ts | 2 +- .../signals/single_search_after.test.ts | 2 +- .../signals/single_search_after.ts | 2 +- .../lib/detection_engine/signals/types.ts | 2 +- .../lib/detection_engine/signals/utils.ts | 2 +- .../detection_engine/tags/read_tags.test.ts | 2 +- .../lib/detection_engine/tags/read_tags.ts | 2 +- x-pack/plugins/siem/server/plugin.ts | 10 +-- .../translations/translations/ja-JP.json | 20 ++--- .../translations/translations/zh-CN.json | 20 ++--- x-pack/plugins/triggers_actions_ui/README.md | 6 +- .../plugins/triggers_actions_ui/kibana.json | 2 +- .../public/application/app.tsx | 4 +- .../threshold/visualization.tsx | 2 +- .../public/application/constants/index.ts | 2 +- .../application/lib/action_variables.ts | 2 +- .../public/application/lib/alert_api.test.ts | 76 +++++++++---------- .../public/application/lib/alert_api.ts | 26 +++---- .../action_form.test.tsx | 2 +- .../actions_connectors_list.test.tsx | 2 +- .../components/view_in_app.test.tsx | 8 +- .../alert_details/components/view_in_app.tsx | 11 +-- .../sections/alert_form/alert_add.test.tsx | 6 +- .../sections/alert_form/alert_edit.test.tsx | 2 +- .../sections/alert_form/alert_form.test.tsx | 4 +- .../sections/alert_form/alert_form.tsx | 5 +- .../sections/alert_form/alert_reducer.test.ts | 2 +- .../components/alerts_list.test.tsx | 2 +- .../alerts_list/components/alerts_list.tsx | 2 +- .../triggers_actions_ui/public/plugin.ts | 6 +- .../triggers_actions_ui/public/types.ts | 4 +- x-pack/plugins/uptime/kibana.json | 2 +- .../lib/adapters/framework/adapter_types.ts | 2 +- .../lib/alerts/__tests__/status_check.test.ts | 6 +- .../uptime/server/lib/alerts/status_check.ts | 2 +- .../plugins/uptime/server/lib/alerts/types.ts | 2 +- x-pack/plugins/uptime/server/uptime_server.ts | 2 +- .../fixtures/plugins/alerts/kibana.json | 2 +- .../plugins/alerts/server/alert_types.ts | 22 +++--- .../fixtures/plugins/alerts/server/plugin.ts | 21 +++-- .../common/lib/alert_utils.ts | 28 ++++--- .../common/lib/object_remover.ts | 5 +- .../security_and_spaces/scenarios.ts | 4 +- .../tests/actions/get_all.ts | 4 +- .../tests/alerting/alerts.ts | 16 ++-- .../tests/alerting/create.ts | 18 ++--- .../tests/alerting/delete.ts | 18 ++--- .../tests/alerting/disable.ts | 12 +-- .../tests/alerting/enable.ts | 16 ++-- .../tests/alerting/find.ts | 20 ++--- .../security_and_spaces/tests/alerting/get.ts | 14 ++-- .../tests/alerting/get_alert_state.ts | 14 ++-- .../tests/alerting/index.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- .../tests/alerting/mute_all.ts | 6 +- .../tests/alerting/mute_instance.ts | 16 ++-- .../tests/alerting/unmute_all.ts | 8 +- .../tests/alerting/unmute_instance.ts | 10 ++- .../tests/alerting/update.ts | 40 +++++----- .../tests/alerting/update_api_key.ts | 16 ++-- .../spaces_only/tests/alerting/alerts_base.ts | 12 +-- .../index_threshold/alert.ts | 4 +- .../spaces_only/tests/alerting/create.ts | 8 +- .../spaces_only/tests/alerting/delete.ts | 8 +- .../spaces_only/tests/alerting/disable.ts | 8 +- .../spaces_only/tests/alerting/enable.ts | 10 +-- .../spaces_only/tests/alerting/find.ts | 12 +-- .../spaces_only/tests/alerting/get.ts | 14 ++-- .../tests/alerting/get_alert_state.ts | 26 ++++--- .../tests/alerting/list_alert_types.ts | 16 +++- .../spaces_only/tests/alerting/mute_all.ts | 6 +- .../tests/alerting/mute_instance.ts | 6 +- .../spaces_only/tests/alerting/unmute_all.ts | 6 +- .../tests/alerting/unmute_instance.ts | 6 +- .../spaces_only/tests/alerting/update.ts | 12 +-- .../tests/alerting/update_api_key.ts | 10 +-- .../case_api_integration/common/config.ts | 1 - .../apps/triggers_actions_ui/alerts.ts | 2 +- .../apps/uptime/alert_flyout.ts | 8 +- .../fixtures/plugins/alerts/kibana.json | 2 +- .../fixtures/plugins/alerts/public/plugin.ts | 10 +-- .../fixtures/plugins/alerts/server/plugin.ts | 18 ++--- .../services/alerting/alerts.ts | 12 +-- x-pack/typings/hapi.d.ts | 4 +- 317 files changed, 766 insertions(+), 734 deletions(-) rename x-pack/plugins/{alerting => alerts}/README.md (91%) rename x-pack/plugins/{alerting => alerts}/common/alert.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/alert_instance.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/alert_navigation.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/alert_task_instance.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/alert_type.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/date_from_string.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/date_from_string.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/index.ts (92%) rename x-pack/plugins/{alerting => alerts}/common/parse_duration.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/common/parse_duration.ts (100%) rename x-pack/plugins/{alerting => alerts}/kibana.json (80%) rename x-pack/plugins/{alerting => alerts}/public/alert_api.test.ts (92%) rename x-pack/plugins/{alerting => alerts}/public/alert_api.ts (84%) rename x-pack/plugins/{alerting => alerts}/public/alert_navigation_registry/alert_navigation_registry.mock.ts (100%) rename x-pack/plugins/{alerting => alerts}/public/alert_navigation_registry/alert_navigation_registry.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/public/alert_navigation_registry/alert_navigation_registry.ts (90%) rename x-pack/plugins/{alerting => alerts}/public/alert_navigation_registry/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/public/alert_navigation_registry/types.ts (100%) rename x-pack/plugins/{alerting => alerts}/public/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/public/mocks.ts (100%) rename x-pack/plugins/{alerting => alerts}/public/plugin.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_instance/alert_instance.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_instance/alert_instance.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_instance/create_alert_instance_factory.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_instance/create_alert_instance_factory.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_instance/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_type_registry.mock.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alert_type_registry.test.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/alert_type_registry.ts (89%) rename x-pack/plugins/{alerting => alerts}/server/alerts_client.mock.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/alerts_client.test.ts (99%) rename x-pack/plugins/{alerting => alerts}/server/alerts_client.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/alerts_client_factory.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/alerts_client_factory.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/constants/plugin.ts (86%) rename x-pack/plugins/{alerting => alerts}/server/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/delete_task_if_it_exists.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/delete_task_if_it_exists.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/is_alert_not_found_error.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/is_alert_not_found_error.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/license_api_access.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/license_state.mock.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/license_state.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/lib/license_state.ts (90%) rename x-pack/plugins/{alerting => alerts}/server/lib/result_type.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/types.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/types.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/validate_alert_type_params.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/lib/validate_alert_type_params.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/mocks.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/plugin.test.ts (97%) rename x-pack/plugins/{alerting => alerts}/server/plugin.ts (99%) rename x-pack/plugins/{alerting => alerts}/server/routes/_mock_handler_arguments.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/routes/create.test.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/routes/create.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/routes/delete.test.ts (97%) rename x-pack/plugins/{alerting => alerts}/server/routes/delete.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/disable.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/disable.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/enable.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/enable.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/find.test.ts (93%) rename x-pack/plugins/{alerting => alerts}/server/routes/find.ts (81%) rename x-pack/plugins/{alerting => alerts}/server/routes/get.test.ts (97%) rename x-pack/plugins/{alerting => alerts}/server/routes/get.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/get_alert_state.test.ts (94%) rename x-pack/plugins/{alerting => alerts}/server/routes/get_alert_state.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/health.test.ts (99%) rename x-pack/plugins/{alerting => alerts}/server/routes/health.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/routes/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/routes/lib/error_handler.ts (94%) create mode 100644 x-pack/plugins/alerts/server/routes/lib/rename_keys.ts rename x-pack/plugins/{alerting => alerts}/server/routes/list_alert_types.test.ts (94%) rename x-pack/plugins/{alerting => alerts}/server/routes/list_alert_types.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/mute_all.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/mute_all.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/mute_instance.test.ts (92%) rename x-pack/plugins/{alerting => alerts}/server/routes/mute_instance.ts (72%) rename x-pack/plugins/{alerting => alerts}/server/routes/unmute_all.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/unmute_all.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/routes/unmute_instance.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/unmute_instance.ts (94%) rename x-pack/plugins/{alerting => alerts}/server/routes/update.test.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/routes/update.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/routes/update_api_key.test.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/routes/update_api_key.ts (96%) rename x-pack/plugins/{alerting => alerts}/server/saved_objects/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/saved_objects/mappings.json (100%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/alert_task_instance.test.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/alert_task_instance.ts (95%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/create_execution_handler.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/create_execution_handler.ts (98%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/get_next_run_at.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/get_next_run_at.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/task_runner.test.ts (99%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/task_runner.ts (99%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/task_runner_factory.test.ts (93%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/task_runner_factory.ts (87%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/transform_action_params.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/task_runner/transform_action_params.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/test_utils/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/types.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/alerts_telemetry.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/alerts_telemetry.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/alerts_usage_collector.test.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/alerts_usage_collector.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/index.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/task.ts (100%) rename x-pack/plugins/{alerting => alerts}/server/usage/types.ts (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c3da7c7f00e96..83f4f90b9204d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -176,7 +176,7 @@ /x-pack/plugins/telemetry_collection_xpack/ @elastic/pulse # Kibana Alerting Services -/x-pack/plugins/alerting/ @elastic/kibana-alerting-services +/x-pack/plugins/alerts/ @elastic/kibana-alerting-services /x-pack/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/event_log/ @elastic/kibana-alerting-services /x-pack/plugins/task_manager/ @elastic/kibana-alerting-services diff --git a/examples/alerting_example/kibana.json b/examples/alerting_example/kibana.json index 76c549adc7970..2b6389649cef9 100644 --- a/examples/alerting_example/kibana.json +++ b/examples/alerting_example/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerting", "actions"], + "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions"], "optionalPlugins": [] } diff --git a/examples/alerting_example/public/alert_types/astros.tsx b/examples/alerting_example/public/alert_types/astros.tsx index 2e263e454fa0c..d52223cb6b988 100644 --- a/examples/alerting_example/public/alert_types/astros.tsx +++ b/examples/alerting_example/public/alert_types/astros.tsx @@ -32,12 +32,12 @@ import { import { i18n } from '@kbn/i18n'; import { flatten } from 'lodash'; import { ALERTING_EXAMPLE_APP_ID, Craft, Operator } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public'; +import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; -export function registerNavigation(alerting: AlertingSetup) { - alerting.registerNavigation( +export function registerNavigation(alerts: AlertingSetup) { + alerts.registerNavigation( ALERTING_EXAMPLE_APP_ID, 'example.people-in-space', (alert: SanitizedAlert) => `/astros/${alert.id}` diff --git a/examples/alerting_example/public/alert_types/index.ts b/examples/alerting_example/public/alert_types/index.ts index 96d9c09d15836..db9f855b573e8 100644 --- a/examples/alerting_example/public/alert_types/index.ts +++ b/examples/alerting_example/public/alert_types/index.ts @@ -19,15 +19,15 @@ import { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public'; +import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; -export function registerNavigation(alerting: AlertingSetup) { +export function registerNavigation(alerts: AlertingSetup) { // register default navigation - alerting.registerDefaultNavigation( + alerts.registerDefaultNavigation( ALERTING_EXAMPLE_APP_ID, (alert: SanitizedAlert) => `/alert/${alert.id}` ); - registerPeopleInSpaceNavigation(alerting); + registerPeopleInSpaceNavigation(alerts); } diff --git a/examples/alerting_example/public/components/view_alert.tsx b/examples/alerting_example/public/components/view_alert.tsx index e418ed91096eb..75a515bfa1b25 100644 --- a/examples/alerting_example/public/components/view_alert.tsx +++ b/examples/alerting_example/public/components/view_alert.tsx @@ -36,7 +36,7 @@ import { Alert, AlertTaskState, BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerting/common'; +} from '../../../../x-pack/plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/examples/alerting_example/public/components/view_astros_alert.tsx index 296269180dd7b..19f235a3f3e4e 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/examples/alerting_example/public/components/view_astros_alert.tsx @@ -38,7 +38,7 @@ import { Alert, AlertTaskState, BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerting/common'; +} from '../../../../x-pack/plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/examples/alerting_example/public/plugin.tsx b/examples/alerting_example/public/plugin.tsx index e3748e3235f47..524ff18bd434e 100644 --- a/examples/alerting_example/public/plugin.tsx +++ b/examples/alerting_example/public/plugin.tsx @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; -import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerting/public'; +import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/public'; import { ChartsPluginStart } from '../../../src/plugins/charts/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../x-pack/plugins/triggers_actions_ui/public'; import { DataPublicPluginStart } from '../../../src/plugins/data/public'; @@ -30,12 +30,12 @@ export type Setup = void; export type Start = void; export interface AlertingExamplePublicSetupDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; } export interface AlertingExamplePublicStartDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; charts: ChartsPluginStart; data: DataPublicPluginStart; @@ -44,7 +44,7 @@ export interface AlertingExamplePublicStartDeps { export class AlertingExamplePlugin implements Plugin { public setup( core: CoreSetup, - { alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps + { alerts, triggers_actions_ui }: AlertingExamplePublicSetupDeps ) { core.application.register({ id: 'AlertingExample', @@ -59,7 +59,7 @@ export class AlertingExamplePlugin implements Plugin { - public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) { - alerting.registerType(alwaysFiringAlert); - alerting.registerType(peopleInSpaceAlert); + public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) { + alerts.registerType(alwaysFiringAlert); + alerts.registerType(peopleInSpaceAlert); } public start() {} diff --git a/rfcs/text/0003_handler_interface.md b/rfcs/text/0003_handler_interface.md index 51e78cf7c9f54..197f2a2012376 100644 --- a/rfcs/text/0003_handler_interface.md +++ b/rfcs/text/0003_handler_interface.md @@ -65,7 +65,7 @@ application.registerApp({ }); // Alerting -alerting.registerType({ +alerts.registerType({ id: 'myAlert', async execute(context, params, state) { const indexPatterns = await context.core.savedObjects.find('indexPattern'); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 7ac27dd47ad64..a479b08ef9069 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -4,7 +4,7 @@ "xpack.actions": "plugins/actions", "xpack.advancedUiActions": "plugins/advanced_ui_actions", "xpack.uiActionsEnhanced": "examples/ui_actions_enhanced_examples", - "xpack.alerting": "plugins/alerting", + "xpack.alerts": "plugins/alerts", "xpack.alertingBuiltins": "plugins/alerting_builtins", "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], diff --git a/x-pack/legacy/plugins/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts index 1a0fecb9ef5b5..ee31a3037a0cb 100644 --- a/x-pack/legacy/plugins/monitoring/index.ts +++ b/x-pack/legacy/plugins/monitoring/index.ts @@ -15,7 +15,7 @@ import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/cons */ const deps = ['kibana', 'elasticsearch', 'xpack_main']; if (KIBANA_ALERTING_ENABLED) { - deps.push(...['alerting', 'actions']); + deps.push(...['alerts', 'actions']); } export const monitoring = (kibana: any) => { return new kibana.Plugin({ diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 847172ae972fd..96d5f04ac088f 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -26,7 +26,7 @@ Table of Contents - [Executor](#executor) - [Example](#example) - [RESTful API](#restful-api) - - [`POST /api/action`: Create action](#post-apiaction-create-action) + - [`POST /api/actions/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionid-delete-action) - [`GET /api/actions`: Get all actions](#get-apiactiongetall-get-all-actions) - [`GET /api/actions/action/{id}`: Get action](#get-apiactionid-get-action) @@ -163,7 +163,7 @@ The built-in email action type provides a good example of creating an action typ Using an action type requires an action to be created that will contain and encrypt configuration for a given action type. See below for CRUD operations using the API. -### `POST /api/action`: Create action +### `POST /api/actions/action`: Create action Payload: diff --git a/x-pack/plugins/alerting_builtins/README.md b/x-pack/plugins/alerting_builtins/README.md index 233984a1ff23f..2944247e4714c 100644 --- a/x-pack/plugins/alerting_builtins/README.md +++ b/x-pack/plugins/alerting_builtins/README.md @@ -1,7 +1,7 @@ # alerting_builtins plugin This plugin provides alertTypes shipped with Kibana for use with the -[the alerting plugin](../alerting/README.md). When enabled, it will register +[the alerts plugin](../alerts/README.md). When enabled, it will register the built-in alertTypes with the alerting plugin, register associated HTTP routes, etc. diff --git a/x-pack/plugins/alerting_builtins/kibana.json b/x-pack/plugins/alerting_builtins/kibana.json index 78de9a1ae0165..cc613d5247ef4 100644 --- a/x-pack/plugins/alerting_builtins/kibana.json +++ b/x-pack/plugins/alerting_builtins/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting"], + "requiredPlugins": ["alerts"], "configPath": ["xpack", "alerting_builtins"], "ui": false } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index.ts index 475efc87b443a..d9232195b0f52 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index.ts @@ -10,7 +10,7 @@ import { register as registerIndexThreshold } from './index_threshold'; interface RegisterBuiltInAlertTypesParams { service: Service; router: IRouter; - alerting: AlertingSetup; + alerts: AlertingSetup; baseRoute: string; } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts index 15139ae34c93d..c3a132bc609d6 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Params } from './alert_type_params'; -import { AlertExecutorOptions } from '../../../../alerting/server'; +import { AlertExecutorOptions } from '../../../../alerts/server'; // alert type context provided to actions diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts index fbe107054ce9d..9787ece323c59 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts @@ -23,14 +23,14 @@ export function getService() { interface RegisterParams { service: Service; router: IRouter; - alerting: AlertingSetup; + alerts: AlertingSetup; baseRoute: string; } export function register(params: RegisterParams) { - const { service, router, alerting, baseRoute } = params; + const { service, router, alerts, baseRoute } = params; - alerting.registerType(getAlertType(service)); + alerts.registerType(getAlertType(service)); const baseBuiltInRoute = `${baseRoute}/index_threshold`; registerRoutes({ service, router, baseRoute: baseBuiltInRoute }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts index 0a4accc983d79..fa991786a60b6 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { times } from 'lodash'; -import { parseDuration } from '../../../../../alerting/server'; +import { parseDuration } from '../../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; // dates as numbers are epoch millis diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index 40e6f187ce18f..a22395cb0961b 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { parseDuration } from '../../../../../alerting/server'; +import { parseDuration } from '../../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './core_query_types'; import { diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts index f93041fa3c142..71a904dcbab3d 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.test.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.test.ts @@ -6,7 +6,7 @@ import { AlertingBuiltinsPlugin } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; -import { alertsMock } from '../../../plugins/alerting/server/mocks'; +import { alertsMock } from '../../alerts/server/mocks'; describe('AlertingBuiltins Plugin', () => { describe('setup()', () => { @@ -22,7 +22,7 @@ describe('AlertingBuiltins Plugin', () => { it('should register built-in alert types', async () => { const alertingSetup = alertsMock.createSetup(); - await plugin.setup(coreSetup, { alerting: alertingSetup }); + await plugin.setup(coreSetup, { alerts: alertingSetup }); expect(alertingSetup.registerType).toHaveBeenCalledTimes(1); @@ -44,7 +44,7 @@ describe('AlertingBuiltins Plugin', () => { it('should return a service in the expected shape', async () => { const alertingSetup = alertsMock.createSetup(); - const service = await plugin.setup(coreSetup, { alerting: alertingSetup }); + const service = await plugin.setup(coreSetup, { alerts: alertingSetup }); expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); }); diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index 9a9483f9c9dfa..12d1b080c7c63 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -22,11 +22,11 @@ export class AlertingBuiltinsPlugin implements Plugin { }; } - public async setup(core: CoreSetup, { alerting }: AlertingBuiltinsDeps): Promise { + public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise { registerBuiltInAlertTypes({ service: this.service, router: core.http.createRouter(), - alerting, + alerts, baseRoute: '/api/alerting_builtins', }); return this.service; diff --git a/x-pack/plugins/alerting_builtins/server/types.ts b/x-pack/plugins/alerting_builtins/server/types.ts index ff07b85fd3038..95d34371a6d1e 100644 --- a/x-pack/plugins/alerting_builtins/server/types.ts +++ b/x-pack/plugins/alerting_builtins/server/types.ts @@ -5,7 +5,7 @@ */ import { Logger, ScopedClusterClient } from '../../../../src/core/server'; -import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; +import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; export { Logger, IRouter } from '../../../../src/core/server'; @@ -14,11 +14,11 @@ export { PluginSetupContract as AlertingSetup, AlertType, AlertExecutorOptions, -} from '../../alerting/server'; +} from '../../alerts/server'; // this plugin's dependendencies export interface AlertingBuiltinsDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; } // external service exposed through plugin setup/start diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerts/README.md similarity index 91% rename from x-pack/plugins/alerting/README.md rename to x-pack/plugins/alerts/README.md index dfa2991895429..811478426a8d3 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerts/README.md @@ -20,20 +20,20 @@ Table of Contents - [Example](#example) - [Alert Navigation](#alert-navigation) - [RESTful API](#restful-api) - - [`POST /api/alert`: Create alert](#post-apialert-create-alert) - - [`DELETE /api/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) - - [`GET /api/alert/_find`: Find alerts](#get-apialertfind-find-alerts) - - [`GET /api/alert/{id}`: Get alert](#get-apialertid-get-alert) - - [`GET /api/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) - - [`GET /api/alert/types`: List alert types](#get-apialerttypes-list-alert-types) - - [`PUT /api/alert/{id}`: Update alert](#put-apialertid-update-alert) - - [`POST /api/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) - - [`POST /api/alert/{id}/_disable`: Disable an alert](#post-apialertiddisable-disable-an-alert) - - [`POST /api/alert/{id}/_mute_all`: Mute all alert instances](#post-apialertidmuteall-mute-all-alert-instances) - - [`POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute`: Mute alert instance](#post-apialertalertidalertinstancealertinstanceidmute-mute-alert-instance) - - [`POST /api/alert/{id}/_unmute_all`: Unmute all alert instances](#post-apialertidunmuteall-unmute-all-alert-instances) - - [`POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance](#post-apialertalertidalertinstancealertinstanceidunmute-unmute-an-alert-instance) - - [`POST /api/alert/{id}/_update_api_key`: Update alert API key](#post-apialertidupdateapikey-update-alert-api-key) + - [`POST /api/alerts/alert`: Create alert](#post-apialert-create-alert) + - [`DELETE /api/alerts/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) + - [`GET /api/alerts/_find`: Find alerts](#get-apialertfind-find-alerts) + - [`GET /api/alerts/alert/{id}`: Get alert](#get-apialertid-get-alert) + - [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) + - [`GET /api/alerts/list_alert_types`: List alert types](#get-apialerttypes-list-alert-types) + - [`PUT /api/alerts/alert/{id}`: Update alert](#put-apialertid-update-alert) + - [`POST /api/alerts/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) + - [`POST /api/alerts/alert/{id}/_disable`: Disable an alert](#post-apialertiddisable-disable-an-alert) + - [`POST /api/alerts/alert/{id}/_mute_all`: Mute all alert instances](#post-apialertidmuteall-mute-all-alert-instances) + - [`POST /api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`: Mute alert instance](#post-apialertalertidalertinstancealertinstanceidmute-mute-alert-instance) + - [`POST /api/alerts/alert/{id}/_unmute_all`: Unmute all alert instances](#post-apialertidunmuteall-unmute-all-alert-instances) + - [`POST /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance](#post-apialertalertidalertinstancealertinstanceidunmute-unmute-an-alert-instance) + - [`POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key](#post-apialertidupdateapikey-update-alert-api-key) - [Schedule Formats](#schedule-formats) - [Alert instance factory](#alert-instance-factory) - [Templating actions](#templating-actions) @@ -78,7 +78,7 @@ Note that the `manage_own_api_key` cluster privilege is not enough - it can be u ### Methods -**server.newPlatform.setup.plugins.alerting.registerType(options)** +**server.newPlatform.setup.plugins.alerts.registerType(options)** The following table describes the properties of the `options` object. @@ -139,7 +139,7 @@ This example receives server and threshold as parameters. It will read the CPU u ```typescript import { schema } from '@kbn/config-schema'; ... -server.newPlatform.setup.plugins.alerting.registerType({ +server.newPlatform.setup.plugins.alerts.registerType({ id: 'my-alert-type', name: 'My alert type', validate: { @@ -220,7 +220,7 @@ server.newPlatform.setup.plugins.alerting.registerType({ This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. ```typescript -server.newPlatform.setup.plugins.alerting.registerType({ +server.newPlatform.setup.plugins.alerts.registerType({ id: 'my-alert-type', name: 'My alert type', validate: { @@ -352,7 +352,7 @@ You can use the `registerNavigation` api to specify as many AlertType specific h Using an alert type requires you to create an alert that will contain parameters and actions for a given alert type. See below for CRUD operations using the API. -### `POST /api/alert`: Create alert +### `POST /api/alerts/alert`: Create alert Payload: @@ -367,7 +367,7 @@ Payload: |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| -### `DELETE /api/alert/{id}`: Delete alert +### `DELETE /api/alerts/alert/{id}`: Delete alert Params: @@ -375,13 +375,13 @@ Params: |---|---|---| |id|The id of the alert you're trying to delete.|string| -### `GET /api/alert/_find`: Find alerts +### `GET /api/alerts/_find`: Find alerts Params: See the saved objects API documentation for find. All the properties are the same except you cannot pass in `type`. -### `GET /api/alert/{id}`: Get alert +### `GET /api/alerts/alert/{id}`: Get alert Params: @@ -389,7 +389,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to get.|string| -### `GET /api/alert/{id}/state`: Get alert state +### `GET /api/alerts/alert/{id}/state`: Get alert state Params: @@ -397,11 +397,11 @@ Params: |---|---|---| |id|The id of the alert whose state you're trying to get.|string| -### `GET /api/alert/types`: List alert types +### `GET /api/alerts/list_alert_types`: List alert types No parameters. -### `PUT /api/alert/{id}`: Update alert +### `PUT /api/alerts/alert/{id}`: Update alert Params: @@ -420,7 +420,7 @@ Payload: |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| -### `POST /api/alert/{id}/_enable`: Enable an alert +### `POST /api/alerts/alert/{id}/_enable`: Enable an alert Params: @@ -428,7 +428,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to enable.|string| -### `POST /api/alert/{id}/_disable`: Disable an alert +### `POST /api/alerts/alert/{id}/_disable`: Disable an alert Params: @@ -436,7 +436,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to disable.|string| -### `POST /api/alert/{id}/_mute_all`: Mute all alert instances +### `POST /api/alerts/alert/{id}/_mute_all`: Mute all alert instances Params: @@ -444,7 +444,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to mute all alert instances for.|string| -### `POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute`: Mute alert instance +### `POST /api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`: Mute alert instance Params: @@ -453,7 +453,7 @@ Params: |alertId|The id of the alert you're trying to mute an instance for.|string| |alertInstanceId|The instance id of the alert instance you're trying to mute.|string| -### `POST /api/alert/{id}/_unmute_all`: Unmute all alert instances +### `POST /api/alerts/alert/{id}/_unmute_all`: Unmute all alert instances Params: @@ -461,7 +461,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to unmute all alert instances for.|string| -### `POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance +### `POST /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance Params: @@ -470,7 +470,7 @@ Params: |alertId|The id of the alert you're trying to unmute an instance for.|string| |alertInstanceId|The instance id of the alert instance you're trying to unmute.|string| -### `POST /api/alert/{id}/_update_api_key`: Update alert API key +### `POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key |Property|Description|Type| |---|---|---| diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert.ts rename to x-pack/plugins/alerts/common/alert.ts diff --git a/x-pack/plugins/alerting/common/alert_instance.ts b/x-pack/plugins/alerts/common/alert_instance.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_instance.ts rename to x-pack/plugins/alerts/common/alert_instance.ts diff --git a/x-pack/plugins/alerting/common/alert_navigation.ts b/x-pack/plugins/alerts/common/alert_navigation.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_navigation.ts rename to x-pack/plugins/alerts/common/alert_navigation.ts diff --git a/x-pack/plugins/alerting/common/alert_task_instance.ts b/x-pack/plugins/alerts/common/alert_task_instance.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_task_instance.ts rename to x-pack/plugins/alerts/common/alert_task_instance.ts diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerts/common/alert_type.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_type.ts rename to x-pack/plugins/alerts/common/alert_type.ts diff --git a/x-pack/plugins/alerting/common/date_from_string.test.ts b/x-pack/plugins/alerts/common/date_from_string.test.ts similarity index 100% rename from x-pack/plugins/alerting/common/date_from_string.test.ts rename to x-pack/plugins/alerts/common/date_from_string.test.ts diff --git a/x-pack/plugins/alerting/common/date_from_string.ts b/x-pack/plugins/alerts/common/date_from_string.ts similarity index 100% rename from x-pack/plugins/alerting/common/date_from_string.ts rename to x-pack/plugins/alerts/common/date_from_string.ts diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerts/common/index.ts similarity index 92% rename from x-pack/plugins/alerting/common/index.ts rename to x-pack/plugins/alerts/common/index.ts index 2574e73dd4f9a..88a8da5a3e575 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -20,4 +20,4 @@ export interface AlertingFrameworkHealth { hasPermanentEncryptionKey: boolean; } -export const BASE_ALERT_API_PATH = '/api/alert'; +export const BASE_ALERT_API_PATH = '/api/alerts'; diff --git a/x-pack/plugins/alerting/common/parse_duration.test.ts b/x-pack/plugins/alerts/common/parse_duration.test.ts similarity index 100% rename from x-pack/plugins/alerting/common/parse_duration.test.ts rename to x-pack/plugins/alerts/common/parse_duration.test.ts diff --git a/x-pack/plugins/alerting/common/parse_duration.ts b/x-pack/plugins/alerts/common/parse_duration.ts similarity index 100% rename from x-pack/plugins/alerting/common/parse_duration.ts rename to x-pack/plugins/alerts/common/parse_duration.ts diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerts/kibana.json similarity index 80% rename from x-pack/plugins/alerting/kibana.json rename to x-pack/plugins/alerts/kibana.json index 59c4bb2221b0a..3509f79dbbe4d 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerts/kibana.json @@ -1,10 +1,10 @@ { - "id": "alerting", + "id": "alerts", "server": true, "ui": true, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "alerting"], + "configPath": ["xpack", "alerts"], "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog"], "optionalPlugins": ["usageCollection", "spaces", "security"] } diff --git a/x-pack/plugins/alerting/public/alert_api.test.ts b/x-pack/plugins/alerts/public/alert_api.test.ts similarity index 92% rename from x-pack/plugins/alerting/public/alert_api.test.ts rename to x-pack/plugins/alerts/public/alert_api.test.ts index 1149e6fc249a9..45b9f5ba8fe2e 100644 --- a/x-pack/plugins/alerting/public/alert_api.test.ts +++ b/x-pack/plugins/alerts/public/alert_api.test.ts @@ -31,7 +31,7 @@ describe('loadAlertTypes', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/types", + "/api/alerts/list_alert_types", ] `); }); @@ -53,7 +53,7 @@ describe('loadAlertType', () => { expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/types", + "/api/alerts/list_alert_types", ] `); }); @@ -111,7 +111,7 @@ describe('loadAlert', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); }); }); @@ -130,7 +130,7 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should parse AlertInstances', async () => { @@ -167,7 +167,7 @@ describe('loadAlertState', () => { }, }, }); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should handle empty response from api', async () => { @@ -175,6 +175,6 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(''); expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); }); diff --git a/x-pack/plugins/alerting/public/alert_api.ts b/x-pack/plugins/alerts/public/alert_api.ts similarity index 84% rename from x-pack/plugins/alerting/public/alert_api.ts rename to x-pack/plugins/alerts/public/alert_api.ts index ee9432885d671..5b7cd2128f386 100644 --- a/x-pack/plugins/alerting/public/alert_api.ts +++ b/x-pack/plugins/alerts/public/alert_api.ts @@ -16,7 +16,7 @@ import { BASE_ALERT_API_PATH, alertStateSchema } from '../common'; import { Alert, AlertType, AlertTaskState } from '../common'; export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/types`); + return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`); } export async function loadAlertType({ @@ -27,11 +27,11 @@ export async function loadAlertType({ id: AlertType['id']; }): Promise { const maybeAlertType = findFirst((type) => type.id === id)( - await http.get(`${BASE_ALERT_API_PATH}/types`) + await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`) ); if (isNone(maybeAlertType)) { throw new Error( - i18n.translate('xpack.alerting.loadAlertType.missingAlertTypeError', { + i18n.translate('xpack.alerts.loadAlertType.missingAlertTypeError', { defaultMessage: 'Alert type "{id}" is not registered.', values: { id, @@ -49,7 +49,7 @@ export async function loadAlert({ http: HttpSetup; alertId: string; }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); + return await http.get(`${BASE_ALERT_API_PATH}/alert/${alertId}`); } type EmptyHttpResponse = ''; @@ -61,7 +61,7 @@ export async function loadAlertState({ alertId: string; }): Promise { return await http - .get(`${BASE_ALERT_API_PATH}/${alertId}/state`) + .get(`${BASE_ALERT_API_PATH}/alert/${alertId}/state`) .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) .then((state: AlertTaskState) => { return pipe( diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.ts similarity index 90% rename from x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.ts index f30629789b4ed..933ed442523c8 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts +++ b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.ts @@ -36,7 +36,7 @@ export class AlertNavigationRegistry { public registerDefault(consumer: string, handler: AlertNavigationHandler) { if (this.hasDefaultHandler(consumer)) { throw new Error( - i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError', { + i18n.translate('xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError', { defaultMessage: 'Default Navigation within "{consumer}" is already registered.', values: { consumer, @@ -54,7 +54,7 @@ export class AlertNavigationRegistry { public register(consumer: string, alertType: AlertType, handler: AlertNavigationHandler) { if (this.hasTypedHandler(consumer, alertType)) { throw new Error( - i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError', { + i18n.translate('xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError', { defaultMessage: 'Navigation for Alert type "{alertType}" within "{consumer}" is already registered.', values: { @@ -78,7 +78,7 @@ export class AlertNavigationRegistry { } throw new Error( - i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', { + i18n.translate('xpack.alerts.alertNavigationRegistry.get.missingNavigationError', { defaultMessage: 'Navigation for Alert type "{alertType}" within "{consumer}" is not registered.', values: { diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/index.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/index.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/index.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/index.ts diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/types.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/types.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/types.ts diff --git a/x-pack/plugins/alerting/public/index.ts b/x-pack/plugins/alerts/public/index.ts similarity index 100% rename from x-pack/plugins/alerting/public/index.ts rename to x-pack/plugins/alerts/public/index.ts diff --git a/x-pack/plugins/alerting/public/mocks.ts b/x-pack/plugins/alerts/public/mocks.ts similarity index 100% rename from x-pack/plugins/alerting/public/mocks.ts rename to x-pack/plugins/alerts/public/mocks.ts diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerts/public/plugin.ts similarity index 100% rename from x-pack/plugins/alerting/public/plugin.ts rename to x-pack/plugins/alerts/public/plugin.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts rename to x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.ts rename to x-pack/plugins/alerts/server/alert_instance/alert_instance.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts rename to x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.test.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts rename to x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/index.ts b/x-pack/plugins/alerts/server/alert_instance/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/index.ts rename to x-pack/plugins/alerts/server/alert_instance/index.ts diff --git a/x-pack/plugins/alerting/server/alert_type_registry.mock.ts b/x-pack/plugins/alerts/server/alert_type_registry.mock.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_type_registry.mock.ts rename to x-pack/plugins/alerts/server/alert_type_registry.mock.ts diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/alert_type_registry.test.ts rename to x-pack/plugins/alerts/server/alert_type_registry.test.ts index e78e5ab7932c2..6d7cf621ab0ca 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -7,7 +7,7 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertTypeRegistry } from './alert_type_registry'; import { AlertType } from './types'; -import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; const taskManager = taskManagerMock.setup(); const alertTypeRegistryParams = { diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts similarity index 89% rename from x-pack/plugins/alerting/server/alert_type_registry.ts rename to x-pack/plugins/alerts/server/alert_type_registry.ts index 0163cb71166e8..8f36afe062aa5 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; -import { RunContext, TaskManagerSetupContract } from '../../../plugins/task_manager/server'; +import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { TaskRunnerFactory } from './task_runner'; import { AlertType } from './types'; @@ -32,7 +32,7 @@ export class AlertTypeRegistry { public register(alertType: AlertType) { if (this.has(alertType.id)) { throw new Error( - i18n.translate('xpack.alerting.alertTypeRegistry.register.duplicateAlertTypeError', { + i18n.translate('xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError', { defaultMessage: 'Alert type "{id}" is already registered.', values: { id: alertType.id, @@ -55,7 +55,7 @@ export class AlertTypeRegistry { public get(id: string): AlertType { if (!this.has(id)) { throw Boom.badRequest( - i18n.translate('xpack.alerting.alertTypeRegistry.get.missingAlertTypeError', { + i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', { defaultMessage: 'Alert type "{id}" is not registered.', values: { id, diff --git a/x-pack/plugins/alerting/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts similarity index 100% rename from x-pack/plugins/alerting/server/alerts_client.mock.ts rename to x-pack/plugins/alerts/server/alerts_client.mock.ts diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/alerts_client.test.ts rename to x-pack/plugins/alerts/server/alerts_client.test.ts index 12106100602e7..9685f58b8fb31 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -7,12 +7,12 @@ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { AlertsClient, CreateOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; -import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { TaskStatus } from '../../../plugins/task_manager/server'; +import { TaskStatus } from '../../task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; -import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock } from '../../actions/server/mocks'; const taskManager = taskManagerMock.start(); @@ -1677,7 +1677,7 @@ describe('find()', () => { }, ], }); - const result = await alertsClient.find(); + const result = await alertsClient.find({ options: {} }); expect(result).toMatchInlineSnapshot(` Object { "data": Array [ diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts similarity index 96% rename from x-pack/plugins/alerting/server/alerts_client.ts rename to x-pack/plugins/alerts/server/alerts_client.ts index 382e9d1a616ad..6b091a5a4503b 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -30,9 +30,9 @@ import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, -} from '../../../plugins/security/server'; -import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; +} from '../../security/server'; +import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; @@ -58,22 +58,29 @@ interface ConstructorOptions { getActionsClient: () => Promise; } -export interface FindOptions { - options?: { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - sortOrder?: string; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string; +export interface MuteOptions extends IndexType { + alertId: string; + alertInstanceId: string; +} + +export interface FindOptions extends IndexType { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + sortOrder?: string; + hasReference?: { + type: string; + id: string; }; + fields?: string[]; + filter?: string; +} + +interface IndexType { + [key: string]: unknown; } export interface FindResult { @@ -225,7 +232,7 @@ export class AlertsClient { } } - public async find({ options = {} }: FindOptions = {}): Promise { + public async find({ options = {} }: { options: FindOptions }): Promise { const { page, per_page: perPage, @@ -533,13 +540,7 @@ export class AlertsClient { }); } - public async muteInstance({ - alertId, - alertInstanceId, - }: { - alertId: string; - alertInstanceId: string; - }) { + public async muteInstance({ alertId, alertInstanceId }: MuteOptions) { const { attributes, version } = await this.savedObjectsClient.get('alert', alertId); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { @@ -652,7 +653,7 @@ export class AlertsClient { ); if (invalidActionGroups.length) { throw Boom.badRequest( - i18n.translate('xpack.alerting.alertsClient.validateActions.invalidGroups', { + i18n.translate('xpack.alerts.alertsClient.validateActions.invalidGroups', { defaultMessage: 'Invalid action groups: {groups}', values: { groups: invalidActionGroups.join(', '), diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/alerts_client_factory.test.ts rename to x-pack/plugins/alerts/server/alerts_client_factory.test.ts index d1a7c60bb9a68..50dafba00a7e4 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -7,12 +7,12 @@ import { Request } from 'hapi'; import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_factory'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { KibanaRequest } from '../../../../src/core/server'; import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; -import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; -import { AuthenticatedUser } from '../../../plugins/security/public'; -import { securityMock } from '../../../plugins/security/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { AuthenticatedUser } from '../../security/public'; +import { securityMock } from '../../security/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; jest.mock('./alerts_client'); diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts similarity index 95% rename from x-pack/plugins/alerting/server/alerts_client_factory.ts rename to x-pack/plugins/alerts/server/alerts_client_factory.ts index 2924736330abd..af546f965d7df 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -8,9 +8,9 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../plugins/security/server'; -import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; +import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; +import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../task_manager/server'; export interface AlertsClientFactoryOpts { logger: Logger; diff --git a/x-pack/plugins/alerting/server/constants/plugin.ts b/x-pack/plugins/alerts/server/constants/plugin.ts similarity index 86% rename from x-pack/plugins/alerting/server/constants/plugin.ts rename to x-pack/plugins/alerts/server/constants/plugin.ts index 9c276ed1d75de..c180b68680841 100644 --- a/x-pack/plugins/alerting/server/constants/plugin.ts +++ b/x-pack/plugins/alerts/server/constants/plugin.ts @@ -7,12 +7,12 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/constants'; export const PLUGIN = { - ID: 'alerting', + ID: 'alerts', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements // all plugins seem to use getI18nName with any // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => - i18n.translate('xpack.alerting.appName', { - defaultMessage: 'Alerting', + i18n.translate('xpack.alerts.appName', { + defaultMessage: 'Alerts', }), }; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerts/server/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/index.ts rename to x-pack/plugins/alerts/server/index.ts diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts b/x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts rename to x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.test.ts diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts b/x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts rename to x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.ts diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerts/server/lib/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/index.ts rename to x-pack/plugins/alerts/server/lib/index.ts diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts b/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts rename to x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts b/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts rename to x-pack/plugins/alerts/server/lib/is_alert_not_found_error.ts diff --git a/x-pack/plugins/alerting/server/lib/license_api_access.ts b/x-pack/plugins/alerts/server/lib/license_api_access.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/license_api_access.ts rename to x-pack/plugins/alerts/server/lib/license_api_access.ts diff --git a/x-pack/plugins/alerting/server/lib/license_state.mock.ts b/x-pack/plugins/alerts/server/lib/license_state.mock.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/license_state.mock.ts rename to x-pack/plugins/alerts/server/lib/license_state.mock.ts diff --git a/x-pack/plugins/alerting/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/lib/license_state.test.ts rename to x-pack/plugins/alerts/server/lib/license_state.test.ts index cbab98a6311dd..50b4e6b4439f7 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { LicenseState } from './license_state'; -import { licensingMock } from '../../../../plugins/licensing/server/mocks'; +import { licensingMock } from '../../../licensing/server/mocks'; describe('license_state', () => { const getRawLicense = jest.fn(); diff --git a/x-pack/plugins/alerting/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts similarity index 90% rename from x-pack/plugins/alerting/server/lib/license_state.ts rename to x-pack/plugins/alerts/server/lib/license_state.ts index 211d7a75dc4fa..ea0106f717b02 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { Observable, Subscription } from 'rxjs'; -import { ILicense } from '../../../../plugins/licensing/common/types'; +import { ILicense } from '../../../licensing/common/types'; import { assertNever } from '../../../../../src/core/server'; import { PLUGIN } from '../constants/plugin'; @@ -43,10 +43,10 @@ export class LicenseState { showAppLink: true, enableAppLink: false, message: i18n.translate( - 'xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage', + 'xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage', { defaultMessage: - 'Alerting is unavailable - license information is not available at this time.', + 'Alerts is unavailable - license information is not available at this time.', } ), }; diff --git a/x-pack/plugins/alerting/server/lib/result_type.ts b/x-pack/plugins/alerts/server/lib/result_type.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/result_type.ts rename to x-pack/plugins/alerts/server/lib/result_type.ts diff --git a/x-pack/plugins/alerting/server/lib/types.test.ts b/x-pack/plugins/alerts/server/lib/types.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/types.test.ts rename to x-pack/plugins/alerts/server/lib/types.test.ts diff --git a/x-pack/plugins/alerting/server/lib/types.ts b/x-pack/plugins/alerts/server/lib/types.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/types.ts rename to x-pack/plugins/alerts/server/lib/types.ts diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts rename to x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts rename to x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts similarity index 100% rename from x-pack/plugins/alerting/server/mocks.ts rename to x-pack/plugins/alerts/server/mocks.ts diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/plugin.test.ts rename to x-pack/plugins/alerts/server/plugin.test.ts index 0411899290ab2..008a9bb804c5b 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -6,8 +6,8 @@ import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; -import { licensingMock } from '../../../plugins/licensing/server/mocks'; -import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { licensingMock } from '../../licensing/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; import { KibanaRequest, CoreSetup } from 'kibana/server'; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts similarity index 99% rename from x-pack/plugins/alerting/server/plugin.ts rename to x-pack/plugins/alerts/server/plugin.ts index e789e655774a0..324bc9fbfb72b 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -53,7 +53,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as ActionsPluginSetupContract, PluginStartContract as ActionsPluginStartContract, -} from '../../../plugins/actions/server'; +} from '../../actions/server'; import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts rename to x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/create.test.ts rename to x-pack/plugins/alerts/server/routes/create.test.ts index a4910495c8a40..9e941903eeaed 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerts/server/routes/create.test.ts @@ -74,7 +74,7 @@ describe('createAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/create.ts rename to x-pack/plugins/alerts/server/routes/create.ts index cc3b7d48162e3..6238fca024e55 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerts/server/routes/create.ts @@ -43,7 +43,7 @@ export const bodySchema = schema.object({ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: BASE_ALERT_API_PATH, + path: `${BASE_ALERT_API_PATH}/alert`, validate: { body: bodySchema, }, diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerts/server/routes/delete.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/routes/delete.test.ts rename to x-pack/plugins/alerts/server/routes/delete.test.ts index 416628d015b5a..9ba4e20312e17 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerts/server/routes/delete.test.ts @@ -29,7 +29,7 @@ describe('deleteAlertRoute', () => { const [config, handler] = router.delete.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerts/server/routes/delete.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/delete.ts rename to x-pack/plugins/alerts/server/routes/delete.ts index f5a7add632edc..2034bd21fbed6 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerts/server/routes/delete.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.delete( { - path: `${BASE_ALERT_API_PATH}/{id}`, + path: `${BASE_ALERT_API_PATH}/alert/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerts/server/routes/disable.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/disable.test.ts rename to x-pack/plugins/alerts/server/routes/disable.test.ts index fde095e9145b6..a82d09854a604 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerts/server/routes/disable.test.ts @@ -29,7 +29,7 @@ describe('disableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_disable"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerts/server/routes/disable.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/disable.ts rename to x-pack/plugins/alerts/server/routes/disable.ts index e1eb089cf4e85..dfc5dfbdd5aa2 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerts/server/routes/disable.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_disable`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_disable`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerts/server/routes/enable.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/enable.test.ts rename to x-pack/plugins/alerts/server/routes/enable.test.ts index e4e89e3f06380..4ee3a12a59dc7 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerts/server/routes/enable.test.ts @@ -28,7 +28,7 @@ describe('enableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_enable"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerts/server/routes/enable.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/enable.ts rename to x-pack/plugins/alerts/server/routes/enable.ts index 90e8f552898d9..b6f86b97d6a3a 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerts/server/routes/enable.ts @@ -24,7 +24,7 @@ const paramSchema = schema.object({ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_enable`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_enable`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerts/server/routes/find.test.ts similarity index 93% rename from x-pack/plugins/alerting/server/routes/find.test.ts rename to x-pack/plugins/alerts/server/routes/find.test.ts index cc601bd42b8ca..f20ee0a54dcd9 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerts/server/routes/find.test.ts @@ -30,7 +30,7 @@ describe('findAlertRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/_find"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -76,13 +76,8 @@ describe('findAlertRoute', () => { Object { "options": Object { "defaultSearchOperator": "OR", - "fields": undefined, - "filter": undefined, "page": 1, "perPage": 1, - "search": undefined, - "sortField": undefined, - "sortOrder": undefined, }, }, ] diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerts/server/routes/find.ts similarity index 81% rename from x-pack/plugins/alerting/server/routes/find.ts rename to x-pack/plugins/alerts/server/routes/find.ts index 3de95c9580cd4..80c9c20eec7da 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerts/server/routes/find.ts @@ -12,10 +12,11 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { FindOptions } from '../../../alerting/server'; import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { renameKeys } from './lib/rename_keys'; +import { FindOptions } from '..'; // config definition const querySchema = schema.object({ @@ -63,31 +64,29 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } const alertsClient = context.alerting.getAlertsClient(); + const query = req.query; - const options: FindOptions['options'] = { - perPage: query.per_page, - page: query.page, - search: query.search, - defaultSearchOperator: query.default_search_operator, - sortField: query.sort_field, - fields: query.fields, - filter: query.filter, - sortOrder: query.sort_order, + const renameMap = { + default_search_operator: 'defaultSearchOperator', + fields: 'fields', + has_reference: 'hasReference', + page: 'page', + per_page: 'perPage', + search: 'search', + sort_field: 'sortField', + sort_order: 'sortOrder', + filter: 'filter', }; + const options = renameKeys>(renameMap, query); + if (query.search_fields) { options.searchFields = Array.isArray(query.search_fields) ? query.search_fields : [query.search_fields]; } - if (query.has_reference) { - options.hasReference = query.has_reference; - } - - const findResult = await alertsClient.find({ - options, - }); + const findResult = await alertsClient.find({ options }); return res.ok({ body: findResult, }); diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/routes/get.test.ts rename to x-pack/plugins/alerts/server/routes/get.test.ts index 7335f13c85a4d..b11224ff4794e 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerts/server/routes/get.test.ts @@ -60,7 +60,7 @@ describe('getAlertRoute', () => { getAlertRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerts/server/routes/get.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/get.ts rename to x-pack/plugins/alerts/server/routes/get.ts index cd78e7fbacddb..ae9ebe1299371 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerts/server/routes/get.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/{id}`, + path: `${BASE_ALERT_API_PATH}/alert/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/get_alert_state.test.ts rename to x-pack/plugins/alerts/server/routes/get_alert_state.test.ts index 20a420ca00986..8c9051093f85b 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts @@ -47,7 +47,7 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -90,7 +90,7 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -133,7 +133,7 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/get_alert_state.ts rename to x-pack/plugins/alerts/server/routes/get_alert_state.ts index a5cb14154db67..b27ae3758e1b9 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/{id}/state`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/state`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerts/server/routes/health.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/routes/health.test.ts rename to x-pack/plugins/alerts/server/routes/health.test.ts index 9b1c95c393f56..b3f41e03ebdc9 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerts/server/routes/health.test.ts @@ -31,7 +31,7 @@ describe('healthRoute', () => { const [config] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/_health"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_health"`); }); it('queries the usage api', async () => { diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerts/server/routes/health.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/health.ts rename to x-pack/plugins/alerts/server/routes/health.ts index fb4c5e76a02c9..b66e28b24e8a7 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerts/server/routes/health.ts @@ -34,7 +34,7 @@ export function healthRoute( ) { router.get( { - path: '/api/alert/_health', + path: '/api/alerts/_health', validate: false, }, router.handleLegacyErrors(async function ( diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerts/server/routes/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/index.ts rename to x-pack/plugins/alerts/server/routes/index.ts diff --git a/x-pack/plugins/alerting/server/routes/lib/error_handler.ts b/x-pack/plugins/alerts/server/routes/lib/error_handler.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/lib/error_handler.ts rename to x-pack/plugins/alerts/server/routes/lib/error_handler.ts index b3cf48c52fe17..e0c620b0670c9 100644 --- a/x-pack/plugins/alerting/server/routes/lib/error_handler.ts +++ b/x-pack/plugins/alerts/server/routes/lib/error_handler.ts @@ -27,7 +27,7 @@ export function handleDisabledApiKeysError( if (isApiKeyDisabledError(e)) { return response.badRequest({ body: new Error( - i18n.translate('xpack.alerting.api.error.disabledApiKeys', { + i18n.translate('xpack.alerts.api.error.disabledApiKeys', { defaultMessage: 'Alerting relies upon API keys which appear to be disabled', }) ), diff --git a/x-pack/plugins/alerts/server/routes/lib/rename_keys.ts b/x-pack/plugins/alerts/server/routes/lib/rename_keys.ts new file mode 100644 index 0000000000000..bfe60a0ecc648 --- /dev/null +++ b/x-pack/plugins/alerts/server/routes/lib/rename_keys.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const renameKeys = , U extends Record>( + keysMap: Record, + obj: Record +): T => + Object.keys(obj).reduce((acc, key) => { + return { + ...acc, + ...{ [keysMap[key] || key]: obj[key] }, + }; + }, {} as T); diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/list_alert_types.test.ts rename to x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index e940b2d102045..3192154f6664c 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -27,7 +27,7 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -89,7 +89,7 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -140,7 +140,7 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/list_alert_types.ts rename to x-pack/plugins/alerts/server/routes/list_alert_types.ts index f5b4e3263f341..51a4558108e29 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.ts @@ -18,7 +18,7 @@ import { BASE_ALERT_API_PATH } from '../../common'; export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/types`, + path: `${BASE_ALERT_API_PATH}/list_alert_types`, validate: {}, options: { tags: ['access:alerting-read'], diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerts/server/routes/mute_all.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/mute_all.test.ts rename to x-pack/plugins/alerts/server/routes/mute_all.test.ts index 5ef9e3694f8f4..bcdb8cbd022ac 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.test.ts @@ -28,7 +28,7 @@ describe('muteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_mute_all"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerts/server/routes/mute_all.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/mute_all.ts rename to x-pack/plugins/alerts/server/routes/mute_all.ts index b43a1ec30ed1f..5b05d7231c385 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_mute_all`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_mute_all`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts similarity index 92% rename from x-pack/plugins/alerting/server/routes/mute_instance.test.ts rename to x-pack/plugins/alerts/server/routes/mute_instance.test.ts index 2e6adedb76df9..c382c12de21cd 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts @@ -29,7 +29,7 @@ describe('muteAlertInstanceRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( - `"/api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute"` + `"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"` ); expect(config.options).toMatchInlineSnapshot(` Object { @@ -45,8 +45,8 @@ describe('muteAlertInstanceRoute', () => { { alertsClient }, { params: { - alertId: '1', - alertInstanceId: '2', + alert_id: '1', + alert_instance_id: '2', }, }, ['noContent'] diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerts/server/routes/mute_instance.ts similarity index 72% rename from x-pack/plugins/alerting/server/routes/mute_instance.ts rename to x-pack/plugins/alerts/server/routes/mute_instance.ts index c0c69fe9653da..00550f4af3418 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.ts @@ -15,16 +15,18 @@ import { import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { renameKeys } from './lib/rename_keys'; +import { MuteOptions } from '../alerts_client'; const paramSchema = schema.object({ - alertId: schema.string(), - alertInstanceId: schema.string(), + alert_id: schema.string(), + alert_instance_id: schema.string(), }); export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{alertId}/alert_instance/{alertInstanceId}/_mute`, + path: `${BASE_ALERT_API_PATH}/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`, validate: { params: paramSchema, }, @@ -42,8 +44,14 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } const alertsClient = context.alerting.getAlertsClient(); - const { alertId, alertInstanceId } = req.params; - await alertsClient.muteInstance({ alertId, alertInstanceId }); + + const renameMap = { + alert_id: 'alertId', + alert_instance_id: 'alertInstanceId', + }; + + const renamedQuery = renameKeys>(renameMap, req.params); + await alertsClient.muteInstance(renamedQuery); return res.noContent(); }) ); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/unmute_all.test.ts rename to x-pack/plugins/alerts/server/routes/unmute_all.test.ts index 1756dbd3fb41d..e13af38fe4cb1 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts @@ -27,7 +27,7 @@ describe('unmuteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_unmute_all"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerts/server/routes/unmute_all.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/unmute_all.ts rename to x-pack/plugins/alerts/server/routes/unmute_all.ts index d4b6e8b7d61b1..1efc9ed40054e 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_unmute_all`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_unmute_all`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/unmute_instance.test.ts rename to x-pack/plugins/alerts/server/routes/unmute_instance.test.ts index 9b9542c606741..b2e2f24e91de9 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts @@ -29,7 +29,7 @@ describe('unmuteAlertInstanceRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( - `"/api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` + `"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` ); expect(config.options).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/unmute_instance.ts rename to x-pack/plugins/alerts/server/routes/unmute_instance.ts index 97ccd8f0adce7..967f9f890c9fb 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.ts @@ -24,7 +24,7 @@ const paramSchema = schema.object({ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{alertId}/alert_instance/{alertInstanceId}/_unmute`, + path: `${BASE_ALERT_API_PATH}/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerts/server/routes/update.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/update.test.ts rename to x-pack/plugins/alerts/server/routes/update.test.ts index cd96f289b8714..c7d23f2670b45 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerts/server/routes/update.test.ts @@ -51,7 +51,7 @@ describe('updateAlertRoute', () => { const [config, handler] = router.put.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerts/server/routes/update.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/update.ts rename to x-pack/plugins/alerts/server/routes/update.ts index 23fea7dc4002f..99b81dfc5b56e 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerts/server/routes/update.ts @@ -44,7 +44,7 @@ const bodySchema = schema.object({ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.put( { - path: `${BASE_ALERT_API_PATH}/{id}`, + path: `${BASE_ALERT_API_PATH}/alert/{id}`, validate: { body: bodySchema, params: paramSchema, diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/update_api_key.test.ts rename to x-pack/plugins/alerts/server/routes/update_api_key.test.ts index 0347feb24a235..babae59553b5b 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts @@ -28,7 +28,7 @@ describe('updateApiKeyRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_update_api_key"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerts/server/routes/update_api_key.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/update_api_key.ts rename to x-pack/plugins/alerts/server/routes/update_api_key.ts index 9d88201d7cd43..4736351a25cbd 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.ts @@ -24,7 +24,7 @@ const paramSchema = schema.object({ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_update_api_key`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_update_api_key`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/saved_objects/index.ts rename to x-pack/plugins/alerts/server/saved_objects/index.ts diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json similarity index 100% rename from x-pack/plugins/alerting/server/saved_objects/mappings.json rename to x-pack/plugins/alerts/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.test.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/task_runner/alert_task_instance.test.ts rename to x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts index 28b3576dffc6e..efac4c5dcdc01 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { AlertTaskInstance, taskInstanceToAlertTaskInstance } from './alert_task_instance'; import uuid from 'uuid'; import { SanitizedAlert } from '../types'; diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts similarity index 95% rename from x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts rename to x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts index 4be506b78493b..a290f3fa33c70 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts +++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; -import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance } from '../../../task_manager/server'; import { SanitizedAlert, AlertTaskState, diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts rename to x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts similarity index 98% rename from x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts rename to x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 61bbab50b1222..3c58c6d9ba288 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -8,7 +8,7 @@ import { pluck } from 'lodash'; import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; -import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; diff --git a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts b/x-pack/plugins/alerts/server/task_runner/get_next_run_at.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts rename to x-pack/plugins/alerts/server/task_runner/get_next_run_at.test.ts diff --git a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.ts b/x-pack/plugins/alerts/server/task_runner/get_next_run_at.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/get_next_run_at.ts rename to x-pack/plugins/alerts/server/task_runner/get_next_run_at.ts diff --git a/x-pack/plugins/alerting/server/task_runner/index.ts b/x-pack/plugins/alerts/server/task_runner/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/index.ts rename to x-pack/plugins/alerts/server/task_runner/index.ts diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/task_runner.test.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 98824b8cf4e1a..983dff86d5602 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -7,10 +7,10 @@ import sinon from 'sinon'; import { schema } from '@kbn/config-schema'; import { AlertExecutorOptions } from '../types'; -import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { TaskRunnerContext } from './task_runner_factory'; import { TaskRunner } from './task_runner'; -import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock } from '../../../actions/server/mocks'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/task_runner.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner.ts index 0831163d1d326..be399893088e3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -7,7 +7,7 @@ import { pick, mapValues, omit, without } from 'lodash'; import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; -import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance } from '../../../task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; import { getNextRunAt } from './get_next_run_at'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts similarity index 93% rename from x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index c1318bac48dfb..7d9710d8a3e08 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -5,9 +5,9 @@ */ import sinon from 'sinon'; -import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; -import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; import { alertsMock } from '../mocks'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts similarity index 87% rename from x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index c50e288d2e520..ca762cf2b2105 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from '../../../../../src/core/server'; -import { RunContext } from '../../../../plugins/task_manager/server'; -import { EncryptedSavedObjectsClient } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; +import { RunContext } from '../../../task_manager/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; import { AlertType, GetBasePathFunction, diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts rename to x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/transform_action_params.ts rename to x-pack/plugins/alerts/server/task_runner/transform_action_params.ts diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerts/server/test_utils/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/test_utils/index.ts rename to x-pack/plugins/alerts/server/test_utils/index.ts diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerts/server/types.ts similarity index 100% rename from x-pack/plugins/alerting/server/types.ts rename to x-pack/plugins/alerts/server/types.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts rename to x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerts/server/usage/alerts_telemetry.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_telemetry.ts rename to x-pack/plugins/alerts/server/usage/alerts_telemetry.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.test.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_usage_collector.test.ts rename to x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts rename to x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts diff --git a/x-pack/plugins/alerting/server/usage/index.ts b/x-pack/plugins/alerts/server/usage/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/index.ts rename to x-pack/plugins/alerts/server/usage/index.ts diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerts/server/usage/task.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/task.ts rename to x-pack/plugins/alerts/server/usage/task.ts diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerts/server/usage/types.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/types.ts rename to x-pack/plugins/alerts/server/usage/types.ts diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index dd89fac66f6e8..2de3c9c97065d 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -15,7 +15,7 @@ "usageCollection", "taskManager", "actions", - "alerting", + "alerts", "observability", "security" ], diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index c3d426a6275a7..0dbde5ea86a18 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -28,7 +28,7 @@ export function ServiceDetails({ tab }: Props) { const canSaveAlerts = !!plugin.core.application.capabilities.apm[ 'alerting:save' ]; - const isAlertingPluginEnabled = 'alerting' in plugin.plugins; + const isAlertingPluginEnabled = 'alerts' in plugin.plugins; const isAlertingAvailable = isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts); diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index e732e695b36b1..76320efe617ea 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -17,7 +17,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { PluginSetupContract as AlertingPluginPublicSetup, PluginStartContract as AlertingPluginPublicStart, -} from '../../alerting/public'; +} from '../../alerts/public'; import { FeaturesPluginSetup } from '../../features/public'; import { DataPublicPluginSetup, @@ -44,7 +44,7 @@ export type ApmPluginSetup = void; export type ApmPluginStart = void; export interface ApmPluginSetupDeps { - alerting?: AlertingPluginPublicSetup; + alerts?: AlertingPluginPublicSetup; data: DataPublicPluginSetup; features: FeaturesPluginSetup; home: HomePublicPluginSetup; @@ -53,7 +53,7 @@ export interface ApmPluginSetupDeps { } export interface ApmPluginStartDeps { - alerting?: AlertingPluginPublicStart; + alerts?: AlertingPluginPublicStart; data: DataPublicPluginStart; home: void; licensing: void; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 8af9f386ecebf..4b8e9cf937a2b 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -5,25 +5,25 @@ */ import { Observable } from 'rxjs'; -import { AlertingPlugin } from '../../../../alerting/server'; +import { AlertingPlugin } from '../../../../alerts/server'; import { ActionsPlugin } from '../../../../actions/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerErrorRateAlertType } from './register_error_rate_alert_type'; import { APMConfig } from '../..'; interface Params { - alerting: AlertingPlugin['setup']; + alerts: AlertingPlugin['setup']; actions: ActionsPlugin['setup']; config$: Observable; } export function registerApmAlerts(params: Params) { registerTransactionDurationAlertType({ - alerting: params.alerting, + alerts: params.alerts, config$: params.config$, }); registerErrorRateAlertType({ - alerting: params.alerting, + alerts: params.alerts, config$: params.config$, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts index ee7bd9eeb4b6f..53843b7f7412b 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts @@ -19,12 +19,12 @@ import { SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerting/server'; +import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; interface RegisterAlertParams { - alerting: AlertingPlugin['setup']; + alerts: AlertingPlugin['setup']; config$: Observable; } @@ -39,10 +39,10 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate]; export function registerErrorRateAlertType({ - alerting, + alerts, config$, }: RegisterAlertParams) { - alerting.registerType({ + alerts.registerType({ id: AlertType.ErrorRate, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index afb402200a07b..1fd1aef4c8b70 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -18,12 +18,12 @@ import { TRANSACTION_DURATION, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerting/server'; +import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; interface RegisterAlertParams { - alerting: AlertingPlugin['setup']; + alerts: AlertingPlugin['setup']; config$: Observable; } @@ -44,10 +44,10 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDuration]; export function registerTransactionDurationAlertType({ - alerting, + alerts, config$, }: RegisterAlertParams) { - alerting.registerType({ + alerts.registerType({ id: AlertType.TransactionDuration, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b9ad14f7ec47d..d32d16d4c3cc8 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -17,7 +17,7 @@ import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; -import { AlertingPlugin } from '../../alerting/server'; +import { AlertingPlugin } from '../../alerts/server'; import { ActionsPlugin } from '../../actions/server'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; @@ -57,7 +57,7 @@ export class APMPlugin implements Plugin { cloud?: CloudSetup; usageCollection?: UsageCollectionSetup; taskManager?: TaskManagerSetupContract; - alerting?: AlertingPlugin['setup']; + alerts?: AlertingPlugin['setup']; actions?: ActionsPlugin['setup']; observability?: ObservabilityPluginSetup; features: FeaturesPluginSetup; @@ -73,9 +73,9 @@ export class APMPlugin implements Plugin { core.savedObjects.registerType(apmIndices); core.savedObjects.registerType(apmTelemetry); - if (plugins.actions && plugins.alerting) { + if (plugins.actions && plugins.alerts) { registerApmAlerts({ - alerting: plugins.alerting, + alerts: plugins.alerts, actions: plugins.actions, config$: mergedConfig$, }); diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index ea66ae7a46d4e..4701182c96813 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -10,7 +10,7 @@ "data", "dataEnhanced", "visTypeTimeseries", - "alerting", + "alerts", "triggers_actions_ui" ], "server": true, diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 4bbbf8dcdee03..d00afbc7b497a 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -13,7 +13,7 @@ import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginSetup } from '../../../../../../plugins/apm/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; -import { PluginSetupContract as AlertingPluginContract } from '../../../../../../plugins/alerting/server'; +import { PluginSetupContract as AlertingPluginContract } from '../../../../../alerts/server'; // NP_TODO: Compose real types from plugins we depend on, no "any" export interface InfraServerPluginDeps { @@ -23,7 +23,7 @@ export interface InfraServerPluginDeps { visTypeTimeseries: VisTypeTimeseriesSetup; features: FeaturesPluginSetup; apm: APMPluginSetup; - alerting: AlertingPluginContract; + alerts: AlertingPluginContract; } export interface CallWithRequestParams extends GenericParams { diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index b36de2a3bd091..5a34a6665e781 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -11,7 +11,7 @@ import { CallWithRequestParams, } from '../../adapters/framework/adapter_types'; import { Comparator, AlertStates, InventoryMetricConditions } from './types'; -import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertServices, AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraSnapshot } from '../../snapshot'; import { parseFilterQuery } from '../../../utils/serialized_query'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 995d415ef3c8f..a3b9e85458416 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -11,12 +11,12 @@ import { LogDocumentCountAlertParams, Criterion, } from '../../../../common/alerting/logs/types'; -import { AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, AlertInstanceMock, AlertServicesMock, -} from '../../../../../alerting/server/mocks'; +} from '../../../../../alerts/server/mocks'; import { libsMock } from './mocks'; interface AlertTestInstance { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index eedaf4202b37d..ee4e1fcb3f6e2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertExecutorOptions, AlertServices } from '../../../../../alerting/server'; +import { AlertExecutorOptions, AlertServices } from '../../../../../alerts/server'; import { AlertStates, Comparator, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index cdb4d2d968479..ed7e82fe29e4c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { PluginSetupContract } from '../../../../../alerting/server'; +import { PluginSetupContract } from '../../../../../alerts/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 19efc88e216ca..8260ebed84622 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,12 +6,12 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; -import { AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, AlertServicesMock, AlertInstanceMock, -} from '../../../../../alerting/server/mocks'; +} from '../../../../../alerts/server/mocks'; import { InfraSources } from '../../sources'; interface AlertTestInstance { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index d1cb60112aa42..233a34a67d1ec 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -17,7 +17,7 @@ import { DOCUMENT_COUNT_I18N, stateToAlertMessage, } from './messages'; -import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertServices, AlertExecutorOptions } from '../../../../../alerts/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; import { InfraBackendLibs } from '../../infra_types'; diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index ae74ed82038fd..989a2917b0520 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract } from '../../../../alerting/server'; +import { PluginSetupContract } from '../../../../alerts/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index a265d53fc1bf8..2fd614830c05d 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -149,7 +149,7 @@ export class InfraServerPlugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerting, this.libs); + registerAlertTypes(plugins.alerts, this.libs); // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 115cc08871ea4..4ed693464712d 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["monitoring"], "requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"], - "optionalPlugins": ["alerting", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"], + "optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"], "server": true, "ui": true } diff --git a/x-pack/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.tsx index cdddbf1031303..6f72168f5069b 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/status.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../../legacy_shims'; -import { Alert, BASE_ALERT_API_PATH } from '../../../../../plugins/alerting/common'; +import { Alert, BASE_ALERT_API_PATH } from '../../../../alerts/common'; import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; import { AlertsConfiguration } from './configuration'; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts index bcc1a8abe5cb0..6262036037712 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts @@ -10,7 +10,7 @@ import { AlertCommonParams, AlertCommonState, AlertClusterStatePerClusterState } import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; import { executeActions } from '../lib/alerts/cluster_state.lib'; import { AlertClusterStateState } from './enums'; -import { alertsMock, AlertServicesMock } from '../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; jest.mock('../lib/alerts/cluster_state.lib', () => ({ executeActions: jest.fn(), diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts index 6567d1c6def31..5b6521179002a 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; -import { AlertType } from '../../../alerting/server'; +import { AlertType } from '../../../alerts/server'; import { executeActions, getUiMessage } from '../lib/alerts/cluster_state.lib'; import { AlertCommonExecutorOptions, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts index 6ffe937679f4c..fb8d10884fdc7 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -16,7 +16,7 @@ import { } from './types'; import { executeActions } from '../lib/alerts/license_expiration.lib'; import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { alertsMock, AlertServicesMock } from '../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; jest.mock('../lib/alerts/license_expiration.lib', () => ({ executeActions: jest.fn(), diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts index 00402bca57a7e..d57f1a7655b18 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; -import { AlertType } from '../../../../plugins/alerting/server'; +import { AlertType } from '../../../alerts/server'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { AlertCommonState, diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index b689d008b51a7..67c74635b4e36 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Moment } from 'moment'; -import { AlertExecutorOptions } from '../../../alerting/server'; +import { AlertExecutorOptions } from '../../../alerts/server'; import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from './enums'; export interface AlertLicense { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts index ae66d603507ca..c4553d87980da 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerting/server'; +import { AlertInstance } from '../../../../alerts/server'; import { AlertCommonCluster, AlertCommonPerClusterMessage, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 7a6c38865ebe8..614658baf5c79 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { Logger } from '../../../../../../src/core/server'; import { AlertCommonPerClusterState } from '../../alerts/types'; -import { AlertsClient } from '../../../../alerting/server'; +import { AlertsClient } from '../../../../alerts/server'; export async function fetchStatus( alertsClient: AlertsClient, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts index 83a9e26e4c589..cfaaeb36535a0 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts @@ -6,7 +6,7 @@ import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'kibana/server'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { AlertServices } from '../../../../alerting/server'; +import { AlertServices } from '../../../../alerts/server'; import { AlertCommonCluster } from '../../alerts/types'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; import { fetchAvailableCcs } from './fetch_available_ccs'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts index cfe9f02b9bd6a..97ef2790b516d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -5,7 +5,7 @@ */ import { Moment } from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerting/server'; +import { AlertInstance } from '../../../../alerts/server'; import { AlertCommonPerClusterMessageLinkToken, AlertCommonPerClusterMessageTimeToken, diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 14bef307b2f85..f4f38f70b1ccb 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -47,7 +47,7 @@ import { MonitoringLicenseService } from './types'; import { PluginStartContract as AlertingPluginStartContract, PluginSetupContract as AlertingPluginSetupContract, -} from '../../alerting/server'; +} from '../../alerts/server'; import { getLicenseExpiration } from './alerts/license_expiration'; import { getClusterState } from './alerts/cluster_state'; import { InfraPluginSetup } from '../../infra/server'; @@ -61,12 +61,12 @@ interface PluginsSetup { usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; features: FeaturesPluginSetupContract; - alerting: AlertingPluginSetupContract; + alerts: AlertingPluginSetupContract; infra: InfraPluginSetup; } interface PluginsStart { - alerting: AlertingPluginStartContract; + alerts: AlertingPluginStartContract; } interface MonitoringCoreConfig { @@ -156,7 +156,7 @@ export class Plugin { await this.licenseService.refresh(); if (KIBANA_ALERTING_ENABLED) { - plugins.alerting.registerType( + plugins.alerts.registerType( getLicenseExpiration( async () => { const coreStart = (await core.getStartServices())[0]; @@ -167,7 +167,7 @@ export class Plugin { config.ui.ccs.enabled ) ); - plugins.alerting.registerType( + plugins.alerts.registerType( getClusterState( async () => { const coreStart = (await core.getStartServices())[0]; @@ -357,7 +357,7 @@ export class Plugin { payload: req.body, getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector, getUiSettingsService: () => context.core.uiSettings.client, - getAlertsClient: () => plugins.alerting.getAlertsClientWithRequest(req), + getAlertsClient: () => plugins.alerts.getAlertsClientWithRequest(req), server: { config: legacyConfigWrapper, newPlatform: { diff --git a/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts index faae1dde83545..9eb2d9abccbd3 100644 --- a/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts @@ -27,7 +27,7 @@ export const filters = t.array(t.unknown); // Filters are not easily type-able y /** * Params is an "object", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts + * @see x-pack/plugins/alerts/common/alert.ts */ export const action_group = t.string; export const action_id = t.string; diff --git a/x-pack/plugins/siem/common/detection_engine/transform_actions.ts b/x-pack/plugins/siem/common/detection_engine/transform_actions.ts index 4ce3823575833..7c15bc143e0fd 100644 --- a/x-pack/plugins/siem/common/detection_engine/transform_actions.ts +++ b/x-pack/plugins/siem/common/detection_engine/transform_actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../alerting/common'; +import { AlertAction } from '../../../alerts/common'; import { RuleAlertAction } from './types'; export const transformRuleToAlertAction = ({ diff --git a/x-pack/plugins/siem/common/detection_engine/types.ts b/x-pack/plugins/siem/common/detection_engine/types.ts index 5a91cfd4809c6..431d716a9f205 100644 --- a/x-pack/plugins/siem/common/detection_engine/types.ts +++ b/x-pack/plugins/siem/common/detection_engine/types.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { AlertAction } from '../../../alerting/common'; +import { AlertAction } from '../../../alerts/common'; export type RuleAlertAction = Omit & { action_type_id: string; diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json index 6b43b41df8eee..df40ad4650f45 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -5,7 +5,7 @@ "configPath": ["xpack", "siem"], "requiredPlugins": [ "actions", - "alerting", + "alerts", "data", "dataEnhanced", "embeddable", diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx index be96ab10bd2b5..43416abe6e288 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { startCase } from 'lodash/fp'; -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; const ActionsDescription = ({ actions }: { actions: AlertAction[] }) => { if (!actions.length) return null; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx index d8064eb4ad391..5823612faac13 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx @@ -18,7 +18,7 @@ import { ActionType, loadActionTypes, } from '../../../../../../triggers_actions_ui/public'; -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { useKibana } from '../../../../common/lib/kibana'; import { FORM_ERRORS_TITLE } from './translations'; diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts index 897568cdbf16e..ab9b88fb81fa7 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts @@ -10,7 +10,7 @@ import { RuleTypeSchema } from '../../../../../common/detection_engine/types'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts + * @see x-pack/plugins/alerts/common/alert.ts */ export const action = t.exact( t.type({ diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts index 92c9780a11722..5f81409010a28 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/README.md b/x-pack/plugins/siem/server/lib/detection_engine/README.md index 695165e1990a9..4c90869a9fe84 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/plugins/siem/server/lib/detection_engine/README.md @@ -152,7 +152,7 @@ logging.events: ``` See these two README.md's pages for more references on the alerting and actions API: -https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md +https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerts/README.md https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions ### Signals API diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts index e0414f842ceb3..440efc8d0d5a3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { createNotifications } from './create_notifications'; describe('createNotifications', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts index 35a737177ad49..a472d8a4df4a4 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert } from '../../../../../alerting/common'; +import { Alert } from '../../../../../alerts/common'; import { APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants'; import { CreateNotificationParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts index 089822f486aeb..2f754c126771a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { deleteNotifications } from './delete_notifications'; import { readNotifications } from './read_notifications'; jest.mock('./read_notifications'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts index b47ea348bd4d6..5d3a328dd6fbb 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FindResult } from '../../../../../alerting/server'; +import { FindResult } from '../../../../../alerts/server'; import { NOTIFICATIONS_ID } from '../../../../common/constants'; import { FindNotificationParams } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts index 69f37da1e225b..038a8916c87d5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { buildSignalsSearchQuery } from './build_signals_query'; interface GetSignalsCount { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts index 961aac15c484d..a46f65da58043 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts @@ -5,7 +5,7 @@ */ import { readNotifications } from './read_notifications'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getNotificationResult, getFindNotificationsResultWithSingleHit, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts index c585c474556a1..fe9101335b4f5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SanitizedAlert } from '../../../../../alerting/common'; +import { SanitizedAlert } from '../../../../../alerts/common'; import { ReadNotificationParams, isAlertType } from './types'; import { findNotifications } from './find_notifications'; import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index e8d778bddadc2..47356679c8075 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -8,7 +8,7 @@ import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult } from '../routes/__mocks__/request_responses'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; import { buildSignalsSearchQuery } from './build_signals_query'; -import { alertsMock, AlertServicesMock } from '../../../../../../plugins/alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { NotificationExecutorOptions } from './types'; jest.mock('./build_signals_query'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts index a0bd5e092c6ea..a26ddfc90434a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -5,7 +5,7 @@ */ import { mapKeys, snakeCase } from 'lodash/fp'; -import { AlertInstance } from '../../../../../alerting/server'; +import { AlertInstance } from '../../../../../alerts/server'; import { RuleTypeParams } from '../types'; export type NotificationRuleTypeParams = RuleTypeParams & { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts index d0bb1bfdfb1c4..1345bf2ac6339 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts @@ -10,8 +10,8 @@ import { AlertType, State, AlertExecutorOptions, -} from '../../../../../alerting/server'; -import { Alert } from '../../../../../alerting/common'; +} from '../../../../../alerts/server'; +import { Alert } from '../../../../../alerts/common'; import { NOTIFICATIONS_ID } from '../../../../common/constants'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts index b9dc42b96696d..c7763c7ed7e77 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { updateNotifications } from './update_notifications'; import { readNotifications } from './read_notifications'; import { createNotifications } from './create_notifications'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts index 5889b0e4dcfb8..17024c7c0d75f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PartialAlert } from '../../../../../alerting/server'; +import { PartialAlert } from '../../../../../alerts/server'; import { readNotifications } from './read_notifications'; import { UpdateNotificationParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts index a24375c368d63..65f38507605a5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -10,7 +10,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../../alerts/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; import { siemMock } from '../../../../mocks'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index ec9e84d4fa6bb..df158d23c0e24 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -23,8 +23,8 @@ import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../ import { BulkError, ImportSuccessError } from '../utils'; import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; -import { PartialAlert } from '../../../../../../alerting/server'; -import { SanitizedAlert } from '../../../../../../alerting/server/types'; +import { PartialAlert } from '../../../../../../alerts/server'; +import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 861e6463533fb..5329ff04435ca 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -8,7 +8,7 @@ import { pickBy, countBy } from 'lodash/fp'; import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import uuid from 'uuid'; -import { PartialAlert, FindResult } from '../../../../../../alerting/server'; +import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 13a5bbd2afc0c..07b891e2bf021 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -13,7 +13,7 @@ import { transformValidateBulkError, } from './validate'; import { getResult } from '../__mocks__/request_responses'; -import { FindResult } from '../../../../../../alerting/server'; +import { FindResult } from '../../../../../../alerts/server'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index 1220b12d1d1b1..5fc239ed48263 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -16,7 +16,7 @@ import { } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { formatErrors } from '../../../../../common/format_errors'; import { exactCheck } from '../../../../../common/exact_check'; -import { PartialAlert, FindResult } from '../../../../../../alerting/server'; +import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 226dea7c20344..66356a1d65352 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 1e2941015b735..013db2020a146 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { createRulesSchema } from './create_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index d28530ffb789e..cb03c4781cb6c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { importRulesSchema, importRulesQuerySchema, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index 755c0b2ccaa3f..218cae68db036 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { patchRulesSchema } from './patch_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index b89df0fc0f3ab..8bda16de97775 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { updateRulesSchema } from './update_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 26c3b29ff2c51..2ff6d6ac646ae 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts index 251f9155f9331..3d5734b13ea48 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index 83cd59f0a1cde..c36f6ca831c57 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getRuleActionsFromSavedObject } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts index 3364827d397d2..c650de2a5e2b9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; import { createRuleActionsSavedObject } from './create_rule_actions_saved_object'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts index c8a3b1bbc38ad..fd3d107103f19 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index f4f0a8042d0a5..f086166d0685e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getMlResult } from '../routes/__mocks__/request_responses'; import { createRules } from './create_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index a007fe35b407e..67e066c6670fb 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -5,7 +5,7 @@ */ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { Alert } from '../../../../../alerting/common'; +import { Alert } from '../../../../../alerts/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts index 6bc5fc2a88b6d..f96a9e38d6a6c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { deleteRules } from './delete_rules'; import { readRules } from './read_rules'; jest.mock('./read_rules'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts index ac600b0b5b218..c634f07387825 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FindResult } from '../../../../../alerting/server'; +import { FindResult } from '../../../../../alerts/server'; import { SIGNALS_ID } from '../../../../common/constants'; import { FindRuleParams } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index d79b428a2f76d..203a23402f097 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getFindResultWithSingleHit, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index 512164fc3d2e1..a3119131a0037 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -5,7 +5,7 @@ */ import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 6df250f1cf513..ee21c33540024 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -9,7 +9,7 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getExportAll } from './get_export_all'; import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../feature_flags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts index 06e70f0bad184..433da2be6b347 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { transformAlertsToRules } from '../routes/rules/utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 092a9a8faf395..b00b7353a370f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,7 @@ import { FindHit, } from '../routes/__mocks__/request_responses'; import * as readRules from './read_rules'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('get_export_by_object_ids', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index beaaaa8701c87..38cf8008f65c8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from '../rules/types'; import { readRules } from './read_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 0266d702b3dcc..7b2cef9060f8c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert } from '../../../../../alerting/common'; -import { AlertsClient } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerts/common'; +import { AlertsClient } from '../../../../../alerts/server'; import { createRules } from './create_rules'; import { PrepackagedRules } from '../types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts index a42500223012e..3c1267c939345 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { patchRules } from './patch_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 6dfb72532afbb..1e728ae7b8d0b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -5,7 +5,7 @@ */ import { defaults } from 'lodash/fp'; -import { PartialAlert } from '../../../../../alerting/server'; +import { PartialAlert } from '../../../../../alerts/server'; import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; import { calculateVersion, calculateName, calculateInterval } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 600848948be0c..ef8e70c78422c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { readRules } from './read_rules'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; export class TestError extends Error { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index 9e0d5b3d05b3f..a8b76aeb8c453 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SanitizedAlert } from '../../../../../alerting/common'; +import { SanitizedAlert } from '../../../../../alerts/common'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; import { ReadRuleParams, isAlertType } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts index d65261549232b..70d53090f81cc 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -13,8 +13,8 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; -import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; -import { Alert, SanitizedAlert } from '../../../../../alerting/common'; +import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; +import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 2d77e9a707f74..ede5c51d1e5e7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { mockPrepackagedRule, getFindResultWithSingleHit, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 5063ddd5e52e2..c793d7eb9b6dc 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { patchRules } from './patch_rules'; import { PrepackagedRules } from '../types'; import { readRules } from './read_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index 13c601b40e4f1..222411deb37ab 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 711f019458096..54031b6e35bf1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -5,7 +5,7 @@ */ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { PartialAlert } from '../../../../../alerting/server'; +import { PartialAlert } from '../../../../../alerts/server'; import { readRules } from './read_rules'; import { UpdateRuleParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts index c5cf85d7ba014..8fceb8ef720b5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertsClient, AlertServices } from '../../../../../alerting/server'; +import { AlertsClient, AlertServices } from '../../../../../alerts/server'; import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; import { RuleActions } from '../rule_actions/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh index b5f272d0a8a09..a052123f0cc34 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh @@ -10,8 +10,8 @@ set -e ./check_env_variables.sh # Example: ./get_alert_instances.sh -# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md#get-apialert_find-find-alerts +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerts/README.md#get-apialert_find-find-alerts curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/_find \ | jq . diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh index 28c250e9368a6..edade604d74ce 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh @@ -10,8 +10,8 @@ set -e ./check_env_variables.sh # Example: ./get_alert_types.sh -# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md#get-apialerttypes-list-alert-types +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerts/README.md#get-apialerttypes-list-alert-types curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/list_alert_types \ | jq . diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 5862e6c481431..80839545951d5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -9,7 +9,7 @@ import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts index 35ec1950cedaa..0930fbdb534f5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts @@ -6,7 +6,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { PartialFilter } from '../types'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; describe('get_filter', () => { let servicesMock: AlertServicesMock; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 3c226130faf25..1630192b3c03a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { assertUnreachable } from '../../../utils/build_query'; import { Filter, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts index 6fc99ada16ece..a4ddec13ac513 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { getInputIndex } from './get_input_output_index'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts index 85e3eeac476e4..c001312fbf2f5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts @@ -5,7 +5,7 @@ */ import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; export const getInputIndex = async ( services: AlertServices, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index a306a016b4205..163ed76d0c6c3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -13,7 +13,7 @@ import { } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import uuid from 'uuid'; import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index 59c685ec3e815..e44b82224d1ce 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AlertServices } from '../../../../../alerts/server'; import { ListClient } from '../../../../../lists/server'; -import { AlertServices } from '../../../../../alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes, RuleAlertParams } from '../types'; import { Logger } from '../../../../../../../src/core/server'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 8e7034b006327..f94eb7006829e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; import { getGapBetweenRuns } from './utils'; import { RuleExecutorOptions } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 265f986533134..8b9fb0574efe9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -18,7 +18,7 @@ import { } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; describe('singleBulkCreate', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index 39aecde470e0b..6f4d01ea73a79 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -6,7 +6,7 @@ import { countBy, isEmpty } from 'lodash'; import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { SignalSearchResponse, BulkResponse } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts index 2aa42234460d8..50b0cb27990f8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts @@ -10,7 +10,7 @@ import { sampleDocSearchResultsWithSortId, } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; describe('singleSearchAfter', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts index a7086a4fb229e..409f374d7df1e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts @@ -5,7 +5,7 @@ */ import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts index 32b13c5251a6b..90497b6e34cb4 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertType, State, AlertExecutorOptions } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleAlertParams, OutputRuleAlertRest } from '../types'; import { SearchResponse } from '../../types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts index 989c919244d65..f0ca08b73fac6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { parseDuration } from '../../../../../alerting/server'; +import { parseDuration } from '../../../../../alerts/server'; import { BulkResponse, BulkResponseErrorAggregation } from './types'; export const generateId = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts index d29d885f9797a..d07fa382e114a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index 003c852cb80af..2bb2b5ec47e2f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -6,7 +6,7 @@ import { has } from 'lodash/fp'; import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { findRules } from '../rules/find_rules'; export interface TagType { diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 5a47efd458888..a8858c91d677c 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -15,7 +15,7 @@ import { PluginInitializerContext, Logger, } from '../../../../src/core/server'; -import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; +import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; @@ -46,7 +46,7 @@ import { EndpointAppContext } from './endpoint/types'; import { IngestIndexPatternRetriever } from './endpoint/alerts/index_pattern'; export interface SetupPlugins { - alerting: AlertingSetup; + alerts: AlertingSetup; encryptedSavedObjects?: EncryptedSavedObjectsSetup; features: FeaturesSetup; licensing: LicensingPluginSetup; @@ -191,7 +191,7 @@ export class Plugin implements IPlugin import('./home')); @@ -34,7 +34,7 @@ export interface AppDeps { dataPlugin: DataPublicPluginStart; charts: ChartsPluginStart; chrome: ChromeStart; - alerting?: AlertingStart; + alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; docLinks: DocLinksStart; toastNotifications: ToastsSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index 84cbc73ca92ca..244d431930f2e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -33,7 +33,7 @@ import { getThresholdAlertVisualizationData } from '../../../../common/lib/index import { AggregationType, Comparator } from '../../../../common/types'; import { AlertsContextValue } from '../../../context/alerts_context'; import { IndexThresholdAlertParams } from './types'; -import { parseDuration } from '../../../../../../alerting/common/parse_duration'; +import { parseDuration } from '../../../../../../alerts/common/parse_duration'; const customTheme = () => { return { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 265cfddab4c06..47b55f44bfb92 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { BASE_ALERT_API_PATH } from '../../../../alerting/common'; +export { BASE_ALERT_API_PATH } from '../../../../alerts/common'; export { BASE_ACTION_API_PATH } from '../../../../actions/common'; export const BASE_PATH = '/management/insightsAndAlerting/triggersActions'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index c35dd06385448..714dc5210e390 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -23,7 +23,7 @@ function prefixKeys(actionVariables: ActionVariable[], prefix: string): ActionVa } // this list should be the same as in: -// x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +// x-pack/plugins/alerts/server/task_runner/transform_action_params.ts function getAlwaysProvidedActionVariables(): ActionVariable[] { const result: ActionVariable[] = []; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index f384a78e2e080..94d9166b40909 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -53,7 +53,7 @@ describe('loadAlertTypes', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/types", + "/api/alerts/list_alert_types", ] `); }); @@ -80,7 +80,7 @@ describe('loadAlert', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); }); }); @@ -99,7 +99,7 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should parse AlertInstances', async () => { @@ -136,7 +136,7 @@ describe('loadAlertState', () => { }, }, }); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should handle empty response from api', async () => { @@ -144,7 +144,7 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(''); expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); }); @@ -162,7 +162,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -192,7 +192,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -226,7 +226,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -260,7 +260,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -295,7 +295,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -330,7 +330,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -356,13 +356,13 @@ describe('deleteAlerts', () => { expect(http.delete.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1", + "/api/alerts/alert/1", ], Array [ - "/api/alert/2", + "/api/alerts/alert/2", ], Array [ - "/api/alert/3", + "/api/alerts/alert/3", ], ] `); @@ -373,7 +373,7 @@ describe('createAlert', () => { test('should call create alert API', async () => { const alertToCreate = { name: 'test', - consumer: 'alerting', + consumer: 'alerts', tags: ['foo'], enabled: true, alertTypeId: 'test', @@ -402,9 +402,9 @@ describe('createAlert', () => { expect(result).toEqual(resolvedValue); expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert", + "/api/alerts/alert", Object { - "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerting\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKey\\":null,\\"apiKeyOwner\\":null}", + "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKey\\":null,\\"apiKeyOwner\\":null}", }, ] `); @@ -415,7 +415,7 @@ describe('updateAlert', () => { test('should call alert update API', async () => { const alertToUpdate = { throttle: '1m', - consumer: 'alerting', + consumer: 'alerts', name: 'test', tags: ['foo'], schedule: { @@ -444,7 +444,7 @@ describe('updateAlert', () => { expect(result).toEqual(resolvedValue); expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/123", + "/api/alerts/alert/123", Object { "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[]}", }, @@ -460,7 +460,7 @@ describe('enableAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_enable", + "/api/alerts/alert/1/_enable", ], ] `); @@ -474,7 +474,7 @@ describe('disableAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_disable", + "/api/alerts/alert/1/_disable", ], ] `); @@ -488,7 +488,7 @@ describe('muteAlertInstance', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/alert_instance/123/_mute", + "/api/alerts/alert/1/alert_instance/123/_mute", ], ] `); @@ -502,7 +502,7 @@ describe('unmuteAlertInstance', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/alert_instance/123/_unmute", + "/api/alerts/alert/1/alert_instance/123/_unmute", ], ] `); @@ -516,7 +516,7 @@ describe('muteAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_mute_all", + "/api/alerts/alert/1/_mute_all", ], ] `); @@ -530,7 +530,7 @@ describe('unmuteAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_unmute_all", + "/api/alerts/alert/1/_unmute_all", ], ] `); @@ -545,13 +545,13 @@ describe('enableAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_enable", + "/api/alerts/alert/1/_enable", ], Array [ - "/api/alert/2/_enable", + "/api/alerts/alert/2/_enable", ], Array [ - "/api/alert/3/_enable", + "/api/alerts/alert/3/_enable", ], ] `); @@ -566,13 +566,13 @@ describe('disableAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_disable", + "/api/alerts/alert/1/_disable", ], Array [ - "/api/alert/2/_disable", + "/api/alerts/alert/2/_disable", ], Array [ - "/api/alert/3/_disable", + "/api/alerts/alert/3/_disable", ], ] `); @@ -587,13 +587,13 @@ describe('muteAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_mute_all", + "/api/alerts/alert/1/_mute_all", ], Array [ - "/api/alert/2/_mute_all", + "/api/alerts/alert/2/_mute_all", ], Array [ - "/api/alert/3/_mute_all", + "/api/alerts/alert/3/_mute_all", ], ] `); @@ -608,13 +608,13 @@ describe('unmuteAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_unmute_all", + "/api/alerts/alert/1/_unmute_all", ], Array [ - "/api/alert/2/_unmute_all", + "/api/alerts/alert/2/_unmute_all", ], Array [ - "/api/alert/3/_unmute_all", + "/api/alerts/alert/3/_unmute_all", ], ] `); @@ -628,7 +628,7 @@ describe('health', () => { expect(http.get.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/_health", + "/api/alerts/_health", ], ] `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts index 2176f978822ca..35fdc3974a296 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts @@ -9,12 +9,12 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { pick } from 'lodash'; -import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerting/common'; +import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerts/common'; import { BASE_ALERT_API_PATH } from '../constants'; import { Alert, AlertType, AlertWithoutId, AlertTaskState } from '../../types'; export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/types`); + return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`); } export async function loadAlert({ @@ -24,7 +24,7 @@ export async function loadAlert({ http: HttpSetup; alertId: string; }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); + return await http.get(`${BASE_ALERT_API_PATH}/alert/${alertId}`); } type EmptyHttpResponse = ''; @@ -36,7 +36,7 @@ export async function loadAlertState({ alertId: string; }): Promise { return await http - .get(`${BASE_ALERT_API_PATH}/${alertId}/state`) + .get(`${BASE_ALERT_API_PATH}/alert/${alertId}/state`) .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) .then((state: AlertTaskState) => { return pipe( @@ -104,7 +104,7 @@ export async function deleteAlerts({ }): Promise<{ successes: string[]; errors: string[] }> { const successes: string[] = []; const errors: string[] = []; - await Promise.all(ids.map((id) => http.delete(`${BASE_ALERT_API_PATH}/${id}`))).then( + await Promise.all(ids.map((id) => http.delete(`${BASE_ALERT_API_PATH}/alert/${id}`))).then( function (fulfilled) { successes.push(...fulfilled); }, @@ -122,7 +122,7 @@ export async function createAlert({ http: HttpSetup; alert: Omit; }): Promise { - return await http.post(`${BASE_ALERT_API_PATH}`, { + return await http.post(`${BASE_ALERT_API_PATH}/alert`, { body: JSON.stringify(alert), }); } @@ -136,7 +136,7 @@ export async function updateAlert({ alert: Pick; id: string; }): Promise { - return await http.put(`${BASE_ALERT_API_PATH}/${id}`, { + return await http.put(`${BASE_ALERT_API_PATH}/alert/${id}`, { body: JSON.stringify( pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions']) ), @@ -144,7 +144,7 @@ export async function updateAlert({ } export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_enable`); } export async function enableAlerts({ @@ -158,7 +158,7 @@ export async function enableAlerts({ } export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_disable`); } export async function disableAlerts({ @@ -180,7 +180,7 @@ export async function muteAlertInstance({ instanceId: string; http: HttpSetup; }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/alert_instance/${instanceId}/_mute`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_mute`); } export async function unmuteAlertInstance({ @@ -192,11 +192,11 @@ export async function unmuteAlertInstance({ instanceId: string; http: HttpSetup; }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/alert_instance/${instanceId}/_unmute`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_unmute`); } export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_mute_all`); } export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { @@ -204,7 +204,7 @@ export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup } export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_unmute_all`); } export async function unmuteAlerts({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index df7d1e64c8e91..7ce952e9b3e0a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -160,7 +160,7 @@ describe('action_form', () => { const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: alertType.id, schedule: { interval: '1m', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 4a4fce5094f0d..3d16bdfa61a00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -13,7 +13,7 @@ import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; -import { alertingPluginMock } from '../../../../../../alerting/public/mocks'; +import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx index e2d9c5cb7fffe..54d335aaba5aa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx @@ -13,7 +13,7 @@ import { ViewInApp } from './view_in_app'; import { useAppDependencies } from '../../../app_context'; jest.mock('../../../app_context', () => { - const alerting = { + const alerts = { getNavigation: jest.fn(async (id) => id === 'alert-with-nav' ? { path: '/alert' } : undefined ), @@ -23,7 +23,7 @@ jest.mock('../../../app_context', () => { useAppDependencies: jest.fn(() => ({ http: jest.fn(), navigateToApp, - alerting, + alerts, legacy: { capabilities: { get: jest.fn(() => ({})), @@ -41,7 +41,7 @@ describe('view in app', () => { describe('link to the app that created the alert', () => { it('is disabled when there is no navigation', async () => { const alert = mockAlert(); - const { alerting } = useAppDependencies(); + const { alerts } = useAppDependencies(); let component: ReactWrapper; await act(async () => { @@ -53,7 +53,7 @@ describe('view in app', () => { expect(component!.find('button').prop('disabled')).toBe(true); expect(component!.text()).toBe('View in app'); - expect(alerting!.getNavigation).toBeCalledWith(alert.id); + expect(alerts!.getNavigation).toBeCalledWith(alert.id); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx index f1f5d8323c22a..5b5de070a94e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx @@ -16,7 +16,7 @@ import { AlertNavigation, AlertStateNavigation, AlertUrlNavigation, -} from '../../../../../../alerting/common'; +} from '../../../../../../alerts/common'; import { Alert } from '../../../../types'; export interface ViewInAppProps { @@ -28,7 +28,7 @@ const NO_NAVIGATION = false; type AlertNavigationLoadingState = AlertNavigation | false | null; export const ViewInApp: React.FunctionComponent = ({ alert }) => { - const { navigateToApp, alerting: maybeAlerting } = useAppDependencies(); + const { navigateToApp, alerts: maybeAlerting } = useAppDependencies(); const [alertNavigation, setAlertNavigation] = useState(null); useEffect(() => { @@ -40,13 +40,14 @@ export const ViewInApp: React.FunctionComponent = ({ alert }) => * navigation isn't supported */ () => setAlertNavigation(NO_NAVIGATION), - (alerting) => - alerting + (alerts) => { + return alerts .getNavigation(alert.id) .then((nav) => (nav ? setAlertNavigation(nav) : setAlertNavigation(NO_NAVIGATION))) .catch(() => { setAlertNavigation(NO_NAVIGATION); - }) + }); + } ) ); }, [alert.id, maybeAlerting]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 56874f3d38b64..f6e8dc49ec275 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -120,11 +120,7 @@ describe('alert_add', () => { }, }} > - {}} - /> + {}} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index bb7e593170f8b..e408c7fcb8144 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -84,7 +84,7 @@ describe('alert_edit', () => { window: '1s', comparator: 'between', }, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: 'my-alert-type', enabled: false, schedule: { interval: '1m' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index ed36bc6c8d580..c9ce2848c5670 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -85,7 +85,7 @@ describe('alert_form', () => { const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerting', + consumer: 'alerts', schedule: { interval: '1m', }, @@ -302,7 +302,7 @@ describe('alert_form', () => { name: 'test', alertTypeId: alertType.id, params: {}, - consumer: 'alerting', + consumer: 'alerts', schedule: { interval: '1m', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 87e018ebe3376..874091b2bb7a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -30,7 +30,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { getDurationNumberInItsUnit, getDurationUnitValue, -} from '../../../../../alerting/common/parse_duration'; +} from '../../../../../alerts/common/parse_duration'; import { loadAlertTypes } from '../../lib/alert_api'; import { actionVariablesFromAlertType } from '../../lib/action_variables'; import { AlertReducerAction } from './alert_reducer'; @@ -168,7 +168,7 @@ export const AlertForm = ({ : null; const alertTypeRegistryList = - alert.consumer === 'alerting' + alert.consumer === 'alerts' ? alertTypeRegistry .list() .filter( @@ -179,6 +179,7 @@ export const AlertForm = ({ .filter( (alertTypeRegistryItem: AlertTypeModel) => alertTypesIndex && + alertTypesIndex[alertTypeRegistryItem.id] && alertTypesIndex[alertTypeRegistryItem.id].producer === alert.consumer ); const alertTypeNodes = alertTypeRegistryList.map(function (item, index) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts index bd320de144024..4e4d8e237aa2f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts @@ -11,7 +11,7 @@ describe('alert reducer', () => { beforeAll(() => { initialAlert = ({ params: {}, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: null, schedule: { interval: '1m', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index cf1524094b41d..a59a4a37bec1f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -15,7 +15,7 @@ import { ValidationResult } from '../../../../types'; import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; -import { alertingPluginMock } from '../../../../../../alerting/public/mocks'; +import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index bd4676cd83071..2929ce6defeaf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -439,7 +439,7 @@ export const AlertsList: React.FunctionComponent = () => { }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index dcf120d37ef8b..3453165a15f69 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -15,7 +15,7 @@ import { TypeRegistry } from './application/type_registry'; import { ManagementStart, ManagementSectionId } from '../../../../src/plugins/management/public'; import { boot } from './application/boot'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; -import { PluginStartContract as AlertingStart } from '../../alerting/public'; +import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; export interface TriggersAndActionsUIPublicPluginSetup { @@ -32,7 +32,7 @@ interface PluginsStart { data: DataPublicPluginStart; charts: ChartsPluginStart; management: ManagementStart; - alerting?: AlertingStart; + alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; } @@ -83,7 +83,7 @@ export class Plugin boot({ dataPlugin: plugins.data, charts: plugins.charts, - alerting: plugins.alerting, + alerts: plugins.alerts, element: params.element, toastNotifications: core.notifications.toasts, http: core.http, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 11152c56c49ec..52179dd35767c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -5,7 +5,7 @@ */ import { HttpSetup, DocLinksStart } from 'kibana/public'; import { ComponentType } from 'react'; -import { ActionGroup } from '../../alerting/common'; +import { ActionGroup } from '../../alerts/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; import { @@ -14,7 +14,7 @@ import { AlertTaskState, RawAlertInstance, AlertingFrameworkHealth, -} from '../../../plugins/alerting/common'; +} from '../../alerts/common'; export { Alert, AlertAction, AlertTaskState, RawAlertInstance, AlertingFrameworkHealth }; export { ActionType }; diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index ce8b64ce07254..5fbd6129fd18f 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "optionalPlugins": ["capabilities", "data", "home"], "requiredPlugins": [ - "alerting", + "alerts", "embeddable", "features", "licensing", diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index f4d1c72770494..5ffc71945caef 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -36,7 +36,7 @@ export interface UptimeCoreSetup { export interface UptimeCorePlugins { features: PluginSetupContract; - alerting: any; + alerts: any; elasticsearch: any; usageCollection: UsageCollectionSetup; } diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 73d104c1d21ae..8c487c85c5720 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -11,12 +11,12 @@ import { fullListByIdAndLocation, } from '../status_check'; import { GetMonitorStatusResult } from '../../requests'; -import { AlertType } from '../../../../../alerting/server'; +import { AlertType } from '../../../../../alerts/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; /** * The alert takes some dependencies as parameters; these are things like @@ -39,7 +39,7 @@ const bootstrapDependencies = (customRequests?: any) => { * This function aims to provide an easy way to give mock props that will * reduce boilerplate for tests. * @param params the params received at alert creation time - * @param services the core services provided by kibana/alerting platforms + * @param services the core services provided by kibana/alerts platforms * @param state the state the alert maintains */ const mockOptions = ( diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 17479bb451b18..3dd1558f5da91 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { isRight } from 'fp-ts/lib/Either'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { i18n } from '@kbn/i18n'; -import { AlertExecutorOptions } from '../../../../alerting/server'; +import { AlertExecutorOptions } from '../../../../alerts/server'; import { UptimeAlertTypeFactory } from './types'; import { GetMonitorStatusResult } from '../requests'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index bc1e82224f7b0..a321cc124ac22 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType } from '../../../../alerting/server'; +import { AlertType } from '../../../../alerts/server'; import { UptimeCoreSetup } from '../adapters'; import { UMServerLibs } from '../lib'; diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts index 180067c0abde2..fb90dfe2be6c5 100644 --- a/x-pack/plugins/uptime/server/uptime_server.ts +++ b/x-pack/plugins/uptime/server/uptime_server.ts @@ -19,6 +19,6 @@ export const initUptimeServer = ( ); uptimeAlertTypeFactories.forEach((alertTypeFactory) => - plugins.alerting.registerType(alertTypeFactory(server, libs)) + plugins.alerts.registerType(alertTypeFactory(server, libs)) ); }; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json index 98c57db16c914..fc42e3199095d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["taskManager", "features", "actions", "alerting"], + "requiredPlugins": ["taskManager", "features", "actions", "alerts"], "optionalPlugins": ["spaces"], "server": true, "ui": false diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index bfabbb8169391..8e3d6b6909a14 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -8,11 +8,11 @@ import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { times } from 'lodash'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; -import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerting/server'; +import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerts/server'; export function defineAlertTypes( core: CoreSetup, - { alerting }: Pick + { alerts }: Pick ) { const clusterClient = core.elasticsearch.legacy.client; const alwaysFiringAlertType: AlertType = { @@ -286,13 +286,13 @@ export function defineAlertTypes( }, async executor(opts: AlertExecutorOptions) {}, }; - alerting.registerType(alwaysFiringAlertType); - alerting.registerType(cumulativeFiringAlertType); - alerting.registerType(neverFiringAlertType); - alerting.registerType(failingAlertType); - alerting.registerType(validationAlertType); - alerting.registerType(authorizationAlertType); - alerting.registerType(noopAlertType); - alerting.registerType(onlyContextVariablesAlertType); - alerting.registerType(onlyStateVariablesAlertType); + alerts.registerType(alwaysFiringAlertType); + alerts.registerType(cumulativeFiringAlertType); + alerts.registerType(neverFiringAlertType); + alerts.registerType(failingAlertType); + alerts.registerType(validationAlertType); + alerts.registerType(authorizationAlertType); + alerts.registerType(noopAlertType); + alerts.registerType(onlyContextVariablesAlertType); + alerts.registerType(onlyStateVariablesAlertType); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index af8dd0282c578..47563f8a5f078 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -6,7 +6,7 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin'; -import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerting/server/plugin'; +import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerts/server/plugin'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { defineAlertTypes } from './alert_types'; @@ -16,7 +16,7 @@ import { defineRoutes } from './routes'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; - alerting: AlertingPluginSetup; + alerts: AlertingPluginSetup; } export interface FixtureStartDeps { @@ -24,17 +24,14 @@ export interface FixtureStartDeps { } export class FixturePlugin implements Plugin { - public setup( - core: CoreSetup, - { features, actions, alerting }: FixtureSetupDeps - ) { + public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { features.registerFeature({ - id: 'alerting', - name: 'Alerting', - app: ['alerting', 'kibana'], + id: 'alerts', + name: 'Alerts', + app: ['alerts', 'kibana'], privileges: { all: { - app: ['alerting', 'kibana'], + app: ['alerts', 'kibana'], savedObject: { all: ['alert'], read: [], @@ -43,7 +40,7 @@ export class FixturePlugin implements Plugin { - const pluginPath = plugin ? `/${plugin}` : ''; return this.supertest - .delete(`${getUrlPrefix(spaceId)}/api${pluginPath}/${type}/${id}`) + .delete(`${getUrlPrefix(spaceId)}/api/${plugin}/${type}/${id}`) .set('kbn-xsrf', 'foo') .expect(204); }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index d58fcd29e29fc..c72ee6a192bf2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -47,7 +47,7 @@ const GlobalRead: User = { kibana: [ { feature: { - alerting: ['read'], + alerts: ['read'], actions: ['read'], }, spaces: ['*'], @@ -75,7 +75,7 @@ const Space1All: User = { kibana: [ { feature: { - alerting: ['all'], + alerts: ['all'], actions: ['all'], }, spaces: ['space1'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 785285f6d455c..45491aa2d28fc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -121,7 +121,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -142,7 +142,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/actions`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 02cd661cbaf04..ab58a205f9d47 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -336,7 +336,7 @@ instanceStateValue: true const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -374,7 +374,7 @@ instanceStateValue: true case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Wait for the task to be attempted once and idle const scheduledActionTask = await retry.try(async () => { @@ -428,7 +428,7 @@ instanceStateValue: true const testStart = new Date(); const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -457,7 +457,7 @@ instanceStateValue: true break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); @@ -490,7 +490,7 @@ instanceStateValue: true break; case 'superuser at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); @@ -532,7 +532,7 @@ instanceStateValue: true .expect(200); objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -571,7 +571,7 @@ instanceStateValue: true break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); @@ -604,7 +604,7 @@ instanceStateValue: true break; case 'superuser at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index ad9fd117c3604..4ca943f3e188a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -43,7 +43,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -72,7 +72,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); expect(response.body).to.eql({ id: response.body.id, name: 'abc', @@ -126,7 +126,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when an alert is disabled ', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(getTestAlertData({ enabled: false })); @@ -145,7 +145,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); expect(response.body.scheduledTaskId).to.eql(undefined); break; default: @@ -155,7 +155,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when alert type is unregistered', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -191,7 +191,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({}); @@ -223,7 +223,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it(`should handle create alert request appropriately when params isn't valid`, async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -260,7 +260,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when interval schedule is wrong syntax', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(getTestAlertData(getTestAlertData({ schedule: { interval: '10x' } }))); @@ -292,7 +292,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when interval schedule is 0', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(getTestAlertData(getTestAlertData({ schedule: { interval: '0s' } }))); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 593ae574e6f34..6f8ae010b9cd8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -32,13 +32,13 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle delete alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -52,7 +52,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { error: 'Not Found', message: 'Not Found', }); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); // Ensure task still exists await getScheduledTask(createdAlert.scheduledTaskId); break; @@ -74,14 +74,14 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it(`shouldn't delete alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -111,7 +111,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it('should still be able to delete alert when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); @@ -129,7 +129,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -143,7 +143,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { error: 'Not Found', message: 'Not Found', }); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); // Ensure task still exists await getScheduledTask(createdAlert.scheduledTaskId); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index dbbccba70a715..589942a7ac11c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -40,11 +40,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte describe(scenario.id, () => { it('should handle disable alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getDisableRequest(createdAlert.id); @@ -86,11 +86,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it('should still be able to disable alert when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -144,11 +144,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it(`shouldn't disable alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix('other')}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add('other', createdAlert.id, 'alert', undefined); + objectRemover.add('other', createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getDisableRequest(createdAlert.id); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index 611556aaf1fef..8cb0eeb0092a3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -40,11 +40,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex describe(scenario.id, () => { it('should handle enable alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getEnableRequest(createdAlert.id); @@ -64,7 +64,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -91,11 +91,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it('should still be able to enable alert when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -127,7 +127,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -154,11 +154,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it(`shouldn't enable alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix('other')}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add('other', createdAlert.id, 'alert', undefined); + objectRemover.add('other', createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getEnableRequest(createdAlert.id); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 1c4d684eb78de..5fe9edeb91ec9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -24,15 +24,17 @@ export default function createFindTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle find alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix( + space.id + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ) .auth(user.username, user.password); @@ -95,7 +97,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { .expect(200); const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -110,13 +112,13 @@ export default function createFindTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get( `${getUrlPrefix( space.id - )}/api/alert/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` + )}/api/alerts/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` ) .auth(user.username, user.password); @@ -174,15 +176,15 @@ export default function createFindTests({ getService }: FtrProviderContext) { it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get( - `${getUrlPrefix('other')}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix('other')}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ) .auth(user.username, user.password); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index 5800273dce75d..a203b7d7c151b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -24,14 +24,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle get alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .auth(user.username, user.password); switch (scenario.id) { @@ -78,14 +78,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { it(`shouldn't get alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}`) .auth(user.username, user.password); expect(response.statusCode).to.eql(404); @@ -114,7 +114,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { it(`should handle get alert request appropriately when alert doesn't exist`, async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/1`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/1`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index 42a6b36df0f97..fd071bd55b377 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -24,14 +24,14 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont describe(scenario.id, () => { it('should handle getAlertState alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/state`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/state`) .auth(user.username, user.password); switch (scenario.id) { @@ -57,14 +57,14 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it(`shouldn't getAlertState for an alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/state`) + .get(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}/state`) .auth(user.username, user.password); expect(response.statusCode).to.eql(404); @@ -93,7 +93,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/1/state`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/1/state`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 91b0ca0a37c92..f14f66f66fe2f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { - describe('Alerting', () => { + describe('Alerts', () => { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./disable')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 4f6b26dfb67fa..dd31e2dbbb5b8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -19,7 +19,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should return 200 with list of alert types', async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/types`) + .get(`${getUrlPrefix(space.id)}/api/alerts/list_alert_types`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts index 0196615629e23..2416bc2ea1d12 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts @@ -32,11 +32,11 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) describe(scenario.id, () => { it('should handle mute alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getMuteAllRequest(createdAlert.id); @@ -56,7 +56,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts index 0c05dbdf55842..c59b9f4503a03 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts @@ -32,11 +32,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider describe(scenario.id, () => { it('should handle mute alert instance request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1'); @@ -56,7 +56,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -76,14 +76,16 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider it('should handle mute alert instance request appropriately and not duplicate mutedInstanceIds when muting an instance already muted', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/alert_instance/1/_mute`) + .post( + `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute` + ) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -105,7 +107,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts index ebe9f1f645ed7..fd22752ccc11a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts @@ -32,14 +32,14 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex describe(scenario.id, () => { it('should handle unmute alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_mute_all`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -61,7 +61,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts index 7142fd7d91adf..72b524282354a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts @@ -32,14 +32,16 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider describe(scenario.id, () => { it('should handle unmute alert instance request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/alert_instance/1/_mute`) + .post( + `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute` + ) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -61,7 +63,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 0af1e22584643..2bcc035beb7a9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -39,11 +39,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle update alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const updatedData = { name: 'bcd', @@ -56,7 +56,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(updatedData); @@ -110,11 +110,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should still be able to update when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -139,7 +139,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(updatedData); @@ -193,14 +193,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -240,14 +240,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately when attempting to change alert type', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -289,7 +289,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/1`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({}); @@ -321,7 +321,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it(`should handle update alert request appropriately when alertTypeConfig isn't valid`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -332,10 +332,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -375,7 +375,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately when interval schedule is wrong syntax', async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/1`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -413,7 +413,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle updates to an alert schedule by rescheduling the underlying task', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -421,7 +421,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await retry.try(async () => { const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; @@ -441,7 +441,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(updatedData); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts index 6349919c15cd2..bf72b970dc0f1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts @@ -32,11 +32,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte describe(scenario.id, () => { it('should handle update alert api key request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); @@ -56,7 +56,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -76,11 +76,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it('should still be able to update API key when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -112,7 +112,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -132,11 +132,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it(`shouldn't update alert api key from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix('other')}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add('other', createdAlert.id, 'alert', undefined); + objectRemover.add('other', createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index d3c914942bd90..8ffe65a8ebb48 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -186,7 +186,7 @@ instanceStateValue: true const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -211,7 +211,7 @@ instanceStateValue: true ); expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); const scheduledActionTask = await retry.try(async () => { const searchResult = await es.search({ index: '.kibana_task_manager', @@ -255,7 +255,7 @@ instanceStateValue: true it('should have proper callCluster and savedObjectsClient authorization for alert type executor', async () => { const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -271,7 +271,7 @@ instanceStateValue: true ); expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); const alertTestRecord = ( await esTestIndexTool.waitForDocs('alert:test.authorization', reference) )[0]; @@ -301,7 +301,7 @@ instanceStateValue: true .expect(200); objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -327,7 +327,7 @@ instanceStateValue: true ); expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); const actionTestRecord = ( await esTestIndexTool.waitForDocs('action:test.authorization', reference) )[0]; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index 353f7d02f6b0b..8412c09eefcda 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -342,7 +342,7 @@ export default function alertTests({ getService }: FtrProviderContext) { }; const { status, body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send({ name: params.name, @@ -372,7 +372,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(status).to.be(200); const alertId = createdAlert.id; - objectRemover.add(Spaces.space1.id, alertId, 'alert', undefined); + objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); return alertId; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index b10c356cf40d5..fa256712a012b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -39,7 +39,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -54,7 +54,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { ); expect(response.status).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, response.body.id, 'alert', 'alerts'); expect(response.body).to.eql({ id: response.body.id, name: 'abc', @@ -104,12 +104,12 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when an alert is disabled ', async () => { const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })); expect(response.status).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, response.body.id, 'alert', 'alerts'); expect(response.body.scheduledTaskId).to.eql(undefined); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts index 3aea982f948ea..e9dfe1607d32d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts @@ -28,13 +28,13 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it('should handle delete alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); await supertest - .delete(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -48,13 +48,13 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it(`shouldn't delete alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); await supertest - .delete(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(404, { statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 7152a76fa167f..afa4f03e23b30 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -35,11 +35,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it('should handle disable alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.disable(createdAlert.id); @@ -61,11 +61,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it(`shouldn't disable alert from another space`, async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.getDisableRequest(createdAlert.id).expect(404, { statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 3d556d0936022..05b212bb064f3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -35,16 +35,16 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it('should handle enable alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.enable(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(typeof updatedAlert.scheduledTaskId).to.eql('string'); @@ -67,11 +67,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it(`shouldn't enable alert from another space`, async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.getEnableRequest(createdAlert.id).expect(404, { statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index f57b136b9637a..06f27d666c3da 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -20,16 +20,16 @@ export default function createFindTests({ getService }: FtrProviderContext) { it('should handle find alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const response = await supertest.get( `${getUrlPrefix( Spaces.space1.id - )}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ); expect(response.status).to.eql(200); @@ -63,17 +63,17 @@ export default function createFindTests({ getService }: FtrProviderContext) { it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await supertest .get( `${getUrlPrefix( Spaces.other.id - )}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ) .expect(200, { page: 1, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 6b216d2ba265f..ff671e16654b5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -20,14 +20,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { it('should handle get alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}` ); expect(response.status).to.eql(200); @@ -57,14 +57,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await supertest - .get(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert/${createdAlert.id}`) .expect(404, { statusCode: 404, error: 'Not Found', @@ -73,7 +73,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); it(`should handle get alert request appropriately when alert doesn't exist`, async () => { - await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1`).expect(404, { + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/1`).expect(404, { statusCode: 404, error: 'Not Found', message: 'Saved object [alert/1] not found', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 06f5f5542780c..d3f08d7c509a0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -21,14 +21,14 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it('should handle getAlertState request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` ); expect(response.status).to.eql(200); @@ -37,7 +37,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it('should fetch updated state', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send({ enabled: true, @@ -51,12 +51,12 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont params: {}, }) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); // wait for alert to actually execute await retry.try(async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` ); expect(response.status).to.eql(200); @@ -65,7 +65,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont }); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` ); expect(response.body.alertTypeState.runCount).to.greaterThan(0); @@ -79,11 +79,13 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont }); it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { - await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/state`).expect(404, { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [alert/1] not found', - }); + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/1/state`) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 845a6f7955739..aef87eefba2ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -15,7 +15,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { describe('list_alert_types', () => { it('should return 200 with list of alert types', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); expect(fixtureAlertType).to.eql({ @@ -32,7 +34,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); it('should return actionVariables with both context and state', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find( @@ -46,7 +50,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); it('should return actionVariables with just context', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find( @@ -60,7 +66,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); it('should return actionVariables with just state', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find( diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts index b2ba38ac98470..f881d0c677bb5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -27,16 +27,16 @@ export default function createMuteTests({ getService }: FtrProviderContext) { it('should handle mute alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteAll(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.muteAll).to.eql(true); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts index d9f52d3321e32..ca0d72aedf7a1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts @@ -27,16 +27,16 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte it('should handle mute alert instance request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteInstance(createdAlert.id, '1'); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mutedInstanceIds).to.eql(['1']); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts index 7c5f1e0a62130..1df99540903d0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -27,17 +27,17 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { it('should handle unmute alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteAll(createdAlert.id); await alertUtils.unmuteAll(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.muteAll).to.eql(false); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts index 86464c3d6bb64..332842ce8015f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts @@ -27,17 +27,17 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon it('should handle unmute alert instance request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteInstance(createdAlert.id, '1'); await alertUtils.unmuteInstance(createdAlert.id, '1'); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mutedInstanceIds).to.eql([]); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index fc0aeb71d9066..b01a1b140f2d6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -20,11 +20,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const updatedData = { name: 'bcd', @@ -37,7 +37,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertest - .put(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .send(updatedData) .expect(200); @@ -75,14 +75,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await supertest - .put(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'bcd', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts index 9c7b4dcc8b1a3..93f91bdc73150 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts @@ -31,16 +31,16 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it('should handle update alert api key appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.updateApiKey(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.apiKeyOwner).to.eql(null); @@ -56,11 +56,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it(`shouldn't update alert api key from another space`, async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.getUpdateApiKeyRequest(createdAlert.id).expect(404, { statusCode: 404, diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 9eb62c2fe07b0..45b34b7d26940 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -78,7 +78,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 89ce3742adf64..13bf47676cc09 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -21,7 +21,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createAlert(overwrites: Record = {}) { const { body: createdAlert } = await supertest - .post(`/api/alert`) + .post(`/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send({ enabled: true, diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index d78053cf926dc..6cb74aff95be2 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -89,7 +89,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // put the fetch code in a retry block with a timeout. let alert: any; await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); + const apiResponse = await supertest.get('/api/alerts/_find?search=uptime-test'); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === 'uptime-test' ); @@ -129,7 +129,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' ); } finally { - await supertest.delete(`/api/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); } }); }); @@ -176,7 +176,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('has created a valid alert with expected parameters', async () => { let alert: any; await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alert/_find?search=${alertId}`); + const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === alertId ); @@ -204,7 +204,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(params).to.eql({}); expect(interval).to.eql('11m'); } finally { - await supertest.delete(`/api/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); } }); }); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json index 1715f30b82260..74f740f52a8b2 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["alerting", "triggers_actions_ui"], + "requiredPlugins": ["alerts", "triggers_actions_ui"], "server": true, "ui": true } diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts index 4c68a3aa15b30..2bc299ede930b 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts @@ -6,21 +6,21 @@ import React from 'react'; import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; -import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerting/public'; -import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerting/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerts/public'; +import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerts/common'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../../../../plugins/triggers_actions_ui/public'; export type Setup = void; export type Start = void; export interface AlertingExamplePublicSetupDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; } export class AlertingFixturePlugin implements Plugin { - public setup(core: CoreSetup, { alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps) { - alerting.registerNavigation( + public setup(core: CoreSetup, { alerts, triggers_actions_ui }: AlertingExamplePublicSetupDeps) { + alerts.registerNavigation( 'consumer-noop', 'test.noop', (alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}` diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 123c0c550e71e..fb431351a382d 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -8,24 +8,24 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as AlertingSetup, AlertType, -} from '../../../../../../plugins/alerting/server'; +} from '../../../../../../plugins/alerts/server'; // this plugin's dependendencies export interface AlertingExampleDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; } export class AlertingFixturePlugin implements Plugin { - public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) { - createNoopAlertType(alerting); - createAlwaysFiringAlertType(alerting); + public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) { + createNoopAlertType(alerts); + createAlwaysFiringAlertType(alerts); } public start() {} public stop() {} } -function createNoopAlertType(alerting: AlertingSetup) { +function createNoopAlertType(alerts: AlertingSetup) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', @@ -34,10 +34,10 @@ function createNoopAlertType(alerting: AlertingSetup) { async executor() {}, producer: 'alerting', }; - alerting.registerType(noopAlertType); + alerts.registerType(noopAlertType); } -function createAlwaysFiringAlertType(alerting: AlertingSetup) { +function createAlwaysFiringAlertType(alerts: AlertingSetup) { // Alert types const alwaysFiringAlertType: any = { id: 'test.always-firing', @@ -63,5 +63,5 @@ function createAlwaysFiringAlertType(alerting: AlertingSetup) { }; }, }; - alerting.registerType(alwaysFiringAlertType); + alerts.registerType(alwaysFiringAlertType); } diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 2a0d28f246765..25f4c6a932d5e 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -38,7 +38,7 @@ export class Alerts { ) { this.log.debug(`creating alert ${name}`); - const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + const { data: alert, status, statusText } = await this.axios.post(`/api/alerts/alert`, { enabled: true, name, tags, @@ -63,7 +63,7 @@ export class Alerts { public async createNoOp(name: string) { this.log.debug(`creating alert ${name}`); - const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + const { data: alert, status, statusText } = await this.axios.post(`/api/alerts/alert`, { enabled: true, name, tags: ['foo'], @@ -96,7 +96,7 @@ export class Alerts { ) { this.log.debug(`creating alert ${name}`); - const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + const { data: alert, status, statusText } = await this.axios.post(`/api/alerts/alert`, { enabled: true, name, tags: ['foo'], @@ -132,7 +132,7 @@ export class Alerts { public async deleteAlert(id: string) { this.log.debug(`deleting alert ${id}`); - const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + const { data: alert, status, statusText } = await this.axios.delete(`/api/alerts/alert/${id}`); if (status !== 204) { throw new Error( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` @@ -144,7 +144,7 @@ export class Alerts { public async getAlertState(id: string) { this.log.debug(`getting alert ${id} state`); - const { data } = await this.axios.get(`/api/alert/${id}/state`); + const { data } = await this.axios.get(`/api/alerts/alert/${id}/state`); return data; } @@ -152,7 +152,7 @@ export class Alerts { this.log.debug(`muting instance ${instanceId} under alert ${id}`); const { data: alert, status, statusText } = await this.axios.post( - `/api/alert/${id}/alert_instance/${instanceId}/_mute` + `/api/alerts/alert/${id}/alert_instance/${instanceId}/_mute` ); if (status !== 204) { throw new Error( diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index ed86a961cd1db..6af723101fc22 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -9,7 +9,7 @@ import 'hapi'; import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main'; import { SecurityPlugin } from '../legacy/plugins/security'; import { ActionsPlugin, ActionsClient } from '../plugins/actions/server'; -import { AlertingPlugin, AlertsClient } from '../plugins/alerting/server'; +import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server'; import { TaskManager } from '../plugins/task_manager/server'; declare module 'hapi' { @@ -21,7 +21,7 @@ declare module 'hapi' { xpack_main: XPackMainPlugin; security?: SecurityPlugin; actions?: ActionsPlugin; - alerting?: AlertingPlugin; + alerts?: AlertingPlugin; task_manager?: TaskManager; } } From e2ddf8bc2d829e357e7f9ea06f8e3d291a86619e Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 2 Jun 2020 11:28:25 +0200 Subject: [PATCH 40/69] [SIEM] Fix draft timeline can be attached to a case (#67844) --- .../components/flyout/header/index.tsx | 4 ++ .../timeline/properties/helpers.tsx | 63 ++++++++-------- .../timeline/properties/index.test.tsx | 71 +++++++++++++++++++ .../components/timeline/properties/index.tsx | 4 ++ .../timeline/properties/properties_right.tsx | 4 ++ .../containers/one/index.gql_query.ts | 1 + 6 files changed, 118 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx index b332260597f22..ab8a24889e9bf 100644 --- a/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx @@ -39,6 +39,7 @@ const StatefulFlyoutHeader = React.memo( title, noteIds, notesById, + status, timelineId, toggleLock, updateDescription, @@ -62,6 +63,7 @@ const StatefulFlyoutHeader = React.memo( isFavorite={isFavorite} title={title} noteIds={noteIds} + status={status} timelineId={timelineId} toggleLock={toggleLock} updateDescription={updateDescription} @@ -94,6 +96,7 @@ const makeMapStateToProps = () => { kqlQuery, title = '', noteIds = emptyNotesId, + status, } = timeline; const history = emptyHistory; // TODO: get history from store via selector @@ -107,6 +110,7 @@ const makeMapStateToProps = () => { isFavorite, isDatepickerLocked: globalInput.linkTo.includes('timeline'), noteIds, + status, title, }; }; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx index 3b1d324f3444d..3ef10d394bc7b 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx @@ -23,6 +23,7 @@ import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { TimelineStatus } from '../../../../../common/types/timeline'; import { Note } from '../../../../common/lib/note'; import { Notes } from '../../notes'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; @@ -119,40 +120,44 @@ Name.displayName = 'Name'; interface NewCaseProps { onClosePopover: () => void; timelineId: string; + timelineStatus: TimelineStatus; timelineTitle: string; } -export const NewCase = React.memo(({ onClosePopover, timelineId, timelineTitle }) => { - const history = useHistory(); - const { savedObjectId } = useSelector((state: State) => - timelineSelectors.selectTimeline(state, timelineId) - ); - const handleClick = useCallback(() => { - onClosePopover(); - history.push({ - pathname: `/${SiemPageName.case}/create`, - state: { - insertTimeline: { - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, +export const NewCase = React.memo( + ({ onClosePopover, timelineId, timelineStatus, timelineTitle }) => { + const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + const handleClick = useCallback(() => { + onClosePopover(); + history.push({ + pathname: `/${SiemPageName.case}/create`, + state: { + insertTimeline: { + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }, }, - }, - }); - }, [onClosePopover, history, timelineId, timelineTitle]); + }); + }, [onClosePopover, history, timelineId, timelineTitle]); - return ( - - {i18n.ATTACH_TIMELINE_TO_NEW_CASE} - - ); -}); + return ( + + {i18n.ATTACH_TIMELINE_TO_NEW_CASE} + + ); + } +); NewCase.displayName = 'NewCase'; interface NewTimelineProps { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx index bfa32fecac89b..8bdec78ec8da2 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx @@ -8,6 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; +import { TimelineStatus } from '../../../../../common/types/timeline'; import { mockGlobalState, apolloClientObservable, @@ -25,6 +26,24 @@ jest.mock('../../../../common/components/utils'); width: mockedWidth, })); +jest.mock('react-redux', () => { + const originalModule = jest.requireActual('react-redux'); + + return { + ...originalModule, + useSelector: jest.fn().mockReturnValue({ savedObjectId: '1' }), + }; +}); + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + describe('Properties', () => { const usersViewing = ['elastic']; @@ -50,6 +69,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -60,7 +80,45 @@ describe('Properties', () => { /> ); + + wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); + expect(wrapper.find('[data-test-subj="timeline-properties"]').exists()).toEqual(true); + expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( + false + ); + }); + + test('renders correctly draft timeline', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); + + expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( + true + ); }); test('it renders an empty star icon when it is NOT a favorite', () => { @@ -76,6 +134,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -103,6 +162,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -132,6 +192,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -159,6 +220,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -191,6 +253,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -222,6 +285,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -256,6 +320,7 @@ describe('Properties', () => { description={description} getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -292,6 +357,7 @@ describe('Properties', () => { description={description} getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -326,6 +392,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -360,6 +427,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -392,6 +460,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -421,6 +490,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -448,6 +518,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx index 502cc85ce907a..d8966a58748ed 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx @@ -6,6 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react'; +import { TimelineStatus } from '../../../../../common/types/timeline'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Note } from '../../../../common/lib/note'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -31,6 +32,7 @@ interface Props { isFavorite: boolean; noteIds: string[]; timelineId: string; + status: TimelineStatus; title: string; toggleLock: ToggleLock; updateDescription: UpdateDescription; @@ -62,6 +64,7 @@ export const Properties = React.memo( isDatepickerLocked, isFavorite, noteIds, + status, timelineId, title, toggleLock, @@ -140,6 +143,7 @@ export const Properties = React.memo( showNotesFromWidth={width < showNotesThreshold} showTimelineModal={showTimelineModal} showUsersView={title.length > 0} + status={status} timelineId={timelineId} title={title} updateDescription={updateDescription} diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx index 4149a958e889d..963b67838e811 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx @@ -22,6 +22,7 @@ import { InspectButton, InspectButtonContainer } from '../../../../common/compon import * as i18n from './translations'; import { AssociateNote } from '../../notes/helpers'; import { Note } from '../../../../common/lib/note'; +import { TimelineStatus } from '../../../../../common/types/timeline'; export const PropertiesRightStyle = styled(EuiFlexGroup)` margin-right: 5px; @@ -79,6 +80,7 @@ interface Props { onCloseTimelineModal: () => void; onOpenTimelineModal: () => void; showTimelineModal: boolean; + status: TimelineStatus; title: string; updateNote: UpdateNote; } @@ -106,6 +108,7 @@ const PropertiesRightComponent: React.FC = ({ onCloseTimelineModal, onOpenTimelineModal, title, + status, }) => ( @@ -142,6 +145,7 @@ const PropertiesRightComponent: React.FC = ({ onClosePopover={onClosePopover} timelineId={timelineId} timelineTitle={title} + timelineStatus={status} /> diff --git a/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts index d70a419b99a3b..47e80b005fb99 100644 --- a/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts @@ -128,6 +128,7 @@ export const oneTimelineQuery = gql` updatedBy version } + status title timelineType templateTimelineId From 8373eb8680935a95c0f61e731b1f1e6332a968de Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 2 Jun 2020 12:47:13 +0300 Subject: [PATCH 41/69] Fix bug in tsvb metric add color rules (#67763) Co-authored-by: Elastic Machine --- .../public/application/components/color_rules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.js index a0cd206822223..46b2886daf36d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.js +++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.js @@ -54,7 +54,7 @@ class ColorRulesUI extends Component { renderRow(row, i, items) { const defaults = { value: 0 }; const model = { ...defaults, ...row }; - const handleAdd = collectionActions.handleAdd.bind(null, this.props); + const handleAdd = () => collectionActions.handleAdd(this.props); const handleDelete = collectionActions.handleDelete.bind(null, this.props, model); const { intl } = this.props; const operatorOptions = [ From 7da774ff5a72dec56c70ece69cb83172e2f9f3cd Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 2 Jun 2020 07:26:22 -0400 Subject: [PATCH 42/69] [Endpoint] add new policy fields (#67323) --- .../policy/store/policy_list/middleware.ts | 8 +- .../policy_list/mock_policy_result_list.ts | 40 ++++ .../pages/policy/view/policy_list.test.tsx | 65 ++++++ .../pages/policy/view/policy_list.tsx | 202 ++++++++++++++---- 4 files changed, 265 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/siem/public/management/pages/policy/store/policy_list/mock_policy_result_list.ts create mode 100644 x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx diff --git a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts index 6054ec34b2d01..7259c0fd4d21c 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts @@ -9,6 +9,7 @@ import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; import { Immutable } from '../../../../../../common/endpoint/types'; +import { initialPolicyListState } from './reducer'; export const policyListMiddlewareFactory: ImmutableMiddlewareFactory> = ( coreStart @@ -19,6 +20,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory GetPolicyListResponse = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const policies = []; + for (let index = 0; index < actualCountToReturn; index++) { + const generator = new EndpointDocGenerator('seed'); + policies.push(generator.generatePolicyDatasource()); + } + const mock: GetPolicyListResponse = { + items: policies, + total, + page: requestPageIndex, + perPage: requestPageSize, + success: true, + }; + return mock; +}; diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx new file mode 100644 index 0000000000000..a2901ab6bfe5c --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import * as reactTestingLibrary from '@testing-library/react'; + +import { PolicyList } from './index'; +import { mockPolicyResultList } from '../store/policy_list/mock_policy_result_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { AppAction } from '../../../../common/store/actions'; + +describe('when on the policies page', () => { + let render: () => ReturnType; + let history: AppContextTestRender['history']; + let store: AppContextTestRender['store']; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + ({ history, store } = mockedContext); + render = () => mockedContext.render(); + }); + + it('should show a table', async () => { + const renderResult = render(); + const table = await renderResult.findByTestId('policyTable'); + expect(table).not.toBeNull(); + }); + + describe('when list data loads', () => { + let firstPolicyID: string; + beforeEach(() => { + reactTestingLibrary.act(() => { + history.push('/management/policy'); + reactTestingLibrary.act(() => { + const policyListData = mockPolicyResultList({ total: 3 }); + firstPolicyID = policyListData.items[0].id; + const action: AppAction = { + type: 'serverReturnedPolicyListData', + payload: { + policyItems: policyListData.items, + total: policyListData.total, + pageSize: policyListData.perPage, + pageIndex: policyListData.page, + }, + }; + store.dispatch(action); + }); + }); + }); + it('should display rows in the table', async () => { + const renderResult = render(); + const rows = await renderResult.findAllByRole('row'); + expect(rows).toHaveLength(4); + }); + it('should display policy name value as a link', async () => { + const renderResult = render(); + const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; + expect(policyNameLink).not.toBeNull(); + expect(policyNameLink.getAttribute('href')).toContain(`policy/${firstPolicyID}`); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx index 3a8004aa2ec6d..2826289dab8e2 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx @@ -4,20 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo } from 'react'; -import { EuiBasicTable, EuiText, EuiTableFieldDataColumnType, EuiLink } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from 'react'; +import { + EuiBasicTable, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiTableFieldDataColumnType, + EuiLink, + EuiPopover, + EuiContextMenuPanelProps, + EuiContextMenuItem, + EuiButtonIcon, + EuiContextMenuPanel, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { useLocation, useHistory } from 'react-router-dom'; -import { - selectApiError, - selectIsLoading, - selectPageIndex, - selectPageSize, - selectPolicyItems, - selectTotal, -} from '../store/policy_list/selectors'; +import { createStructuredSelector } from 'reselect'; +import { CreateStructuredSelector } from '../../../../common/store'; +import * as selectors from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -27,11 +34,53 @@ import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; import { ManagementPageView } from '../../../components/management_page_view'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { getManagementUrl } from '../../../common/routing'; +import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time'; interface TableChangeCallbackArguments { page: { index: number; size: number }; } +interface PackageData { + name: string; + title: string; + version: string; +} + +const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); + +// eslint-disable-next-line react/display-name +export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['items'] }>( + ({ items }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + ); + } +); + const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ name, route, @@ -40,24 +89,32 @@ const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ const clickHandler = useNavigateByRouterEventHandler(route); return ( // eslint-disable-next-line @elastic/eui/href-or-on-click - + {name} ); }; +const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const PolicyList = React.memo(() => { const { services, notifications } = useKibana(); const history = useHistory(); const location = useLocation(); const dispatch = useDispatch<(action: PolicyListAction) => void>(); - const policyItems = usePolicyListSelector(selectPolicyItems); - const pageIndex = usePolicyListSelector(selectPageIndex); - const pageSize = usePolicyListSelector(selectPageSize); - const totalItemCount = usePolicyListSelector(selectTotal); - const loading = usePolicyListSelector(selectIsLoading); - const apiError = usePolicyListSelector(selectApiError); + const { + selectPolicyItems: policyItems, + selectPageIndex: pageIndex, + selectPageSize: pageSize, + selectTotal: totalItemCount, + selectIsLoading: loading, + selectApiError: apiError, + } = usePolicyListSelector(selector); useEffect(() => { if (apiError) { @@ -94,58 +151,110 @@ export const PolicyList = React.memo(() => { defaultMessage: 'Policy Name', }), // eslint-disable-next-line react/display-name - render: (value: string, item: Immutable) => { + render: (name: string, item: Immutable) => { const routePath = getManagementUrl({ name: 'policyDetails', policyId: item.id, excludePrefix: true, }); const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id }); - return ; + return ( + + + + + + + + + + + ); }, - truncateText: true, }, { - field: 'revision', - name: i18n.translate('xpack.siem.endpoint.policyList.revisionField', { - defaultMessage: 'Revision', + field: 'created_by', + name: i18n.translate('xpack.siem.endpoint.policyList.createdBy', { + defaultMessage: 'Created By', }), - dataType: 'number', + truncateText: true, }, { - field: 'package', - name: i18n.translate('xpack.siem.endpoint.policyList.versionField', { - defaultMessage: 'Version', + field: 'created_at', + name: i18n.translate('xpack.siem.endpoint.policyList.createdAt', { + defaultMessage: 'Created Date', }), - render(pkg) { - return `${pkg.title} v${pkg.version}`; + render(createdAt: string) { + return ; }, }, { - field: 'description', - name: i18n.translate('xpack.siem.endpoint.policyList.descriptionField', { - defaultMessage: 'Description', + field: 'updated_by', + name: i18n.translate('xpack.siem.endpoint.policyList.updatedBy', { + defaultMessage: 'Last Updated By', }), truncateText: true, }, { - field: 'config_id', - name: i18n.translate('xpack.siem.endpoint.policyList.agentConfigField', { - defaultMessage: 'Agent Configuration', + field: 'updated_at', + name: i18n.translate('xpack.siem.endpoint.policyList.updatedAt', { + defaultMessage: 'Last Updated', }), - render(version: string) { - return ( - - {version} - - ); + render(updatedAt: string) { + return ; }, }, + { + field: 'package', + name: i18n.translate('xpack.siem.endpoint.policyList.versionFieldLabel', { + defaultMessage: 'Version', + }), + render(pkg: Immutable) { + return i18n.translate('xpack.siem.endpoint.policyList.versionField', { + defaultMessage: '{title} v{version}', + values: { + title: pkg.title, + version: pkg.version, + }, + }); + }, + }, + { + field: '', + name: 'Actions', + actions: [ + { + // eslint-disable-next-line react/display-name + render: (item: Immutable) => { + return ( + + + + + , + ]} + /> + ); + }, + }, + ], + }, ], [services.application] ); @@ -174,6 +283,7 @@ export const PolicyList = React.memo(() => { pagination={paginationSetup} onChange={handleTableChange} data-test-subj="policyTable" + hasActions={false} /> From 7d0ffb53bcb9d302e9f1f675b70d36b235f78e8d Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 2 Jun 2020 08:32:24 -0400 Subject: [PATCH 43/69] install default packages in parallel (#67893) --- .../server/services/epm/packages/install.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 7c0d5d571f6a5..736711f9152e9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -52,22 +52,22 @@ export async function ensureInstalledDefaultPackages( const installations = []; for (const pkgName in DefaultPackages) { if (!DefaultPackages.hasOwnProperty(pkgName)) continue; - const installation = await ensureInstalledPackage({ + const installation = ensureInstalledPackage({ savedObjectsClient, pkgName, callCluster, }); - if (installation) installations.push(installation); + installations.push(installation); } - return installations; + return Promise.all(installations); } export async function ensureInstalledPackage(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; callCluster: CallESAsCurrentUser; -}): Promise { +}): Promise { const { savedObjectsClient, pkgName, callCluster } = options; const installedPackage = await getInstallation({ savedObjectsClient, pkgName }); if (installedPackage) { @@ -79,7 +79,9 @@ export async function ensureInstalledPackage(options: { pkgName, callCluster, }); - return await getInstallation({ savedObjectsClient, pkgName }); + const installation = await getInstallation({ savedObjectsClient, pkgName }); + if (!installation) throw new Error(`could not get installation ${pkgName}`); + return installation; } export async function installPackage(options: { From a091124fab89573e6744582a09d4eea19e4c28f7 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 2 Jun 2020 15:15:21 +0200 Subject: [PATCH 44/69] Move application.applications$ to public contract (#67463) * expose applications$ on public contract * review comments --- ...e-public.applicationstart.applications_.md | 18 ++++++ ...ana-plugin-core-public.applicationstart.md | 1 + ...ana-plugin-core-public.legacyapp.appurl.md | 11 ++++ ...-public.legacyapp.disablesuburltracking.md | 11 ++++ ...-core-public.legacyapp.linktolastsuburl.md | 11 ++++ .../kibana-plugin-core-public.legacyapp.md | 22 +++++++ ...plugin-core-public.legacyapp.suburlbase.md | 11 ++++ .../core/public/kibana-plugin-core-public.md | 3 + ...kibana-plugin-core-public.publicappinfo.md | 15 +++++ ...-plugin-core-public.publiclegacyappinfo.md | 15 +++++ .../application/application_service.mock.ts | 7 ++- .../application/application_service.test.ts | 15 ++++- .../application/application_service.tsx | 7 ++- src/core/public/application/index.ts | 4 +- src/core/public/application/types.ts | 42 +++++++++----- src/core/public/application/utils.test.ts | 57 ++++++++++++++++++- src/core/public/application/utils.ts | 18 +++++- src/core/public/chrome/chrome_service.test.ts | 7 ++- .../chrome/nav_links/to_nav_link.test.ts | 9 +-- .../public/chrome/nav_links/to_nav_link.ts | 9 ++- src/core/public/index.ts | 3 + src/core/public/legacy/legacy_service.ts | 1 + src/core/public/plugins/plugin_context.ts | 1 + src/core/public/public.api.md | 23 ++++++++ 24 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappinfo.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md new file mode 100644 index 0000000000000..d428faa500faf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) > [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) + +## ApplicationStart.applications$ property + +Observable emitting the list of currently registered apps and their associated status. + +Signature: + +```typescript +applications$: Observable>; +``` + +## Remarks + +Applications disabled by [Capabilities](./kibana-plugin-core-public.capabilities.md) will not be present in the map. Applications manually disabled from the client-side using an [application updater](./kibana-plugin-core-public.appupdater.md) are present, with their status properly set as `inaccessible`. + diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md index 6f45bab3ebd2d..896de2de32dd5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md @@ -15,6 +15,7 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | +| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | Observable<ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo>> | Observable emitting the list of currently registered apps and their associated status. | | [capabilities](./kibana-plugin-core-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | | [currentAppId$](./kibana-plugin-core-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md new file mode 100644 index 0000000000000..292bf29962839 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) + +## LegacyApp.appUrl property + +Signature: + +```typescript +appUrl: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md new file mode 100644 index 0000000000000..af4d0eb7969d3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) + +## LegacyApp.disableSubUrlTracking property + +Signature: + +```typescript +disableSubUrlTracking?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md new file mode 100644 index 0000000000000..fa1314b74fd83 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) + +## LegacyApp.linkToLastSubUrl property + +Signature: + +```typescript +linkToLastSubUrl?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.md new file mode 100644 index 0000000000000..06533aaa99170 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) + +## LegacyApp interface + + +Signature: + +```typescript +export interface LegacyApp extends AppBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) | string | | +| [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) | boolean | | +| [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) | boolean | | +| [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md new file mode 100644 index 0000000000000..44a1e52ccd244 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) + +## LegacyApp.subUrlBase property + +Signature: + +```typescript +subUrlBase?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b2524ec48c757..9e4afe0f5133c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -90,6 +90,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [ImageValidation](./kibana-plugin-core-public.imagevalidation.md) | | | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | +| [LegacyApp](./kibana-plugin-core-public.legacyapp.md) | | | [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) | | @@ -162,6 +163,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | +| [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | +| [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) | Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md new file mode 100644 index 0000000000000..c70f3a97a8882 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) + +## PublicAppInfo type + +Public information about a registered [application](./kibana-plugin-core-public.app.md) + +Signature: + +```typescript +export declare type PublicAppInfo = Omit & { + legacy: false; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md new file mode 100644 index 0000000000000..cc3e9de3193cb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) + +## PublicLegacyAppInfo type + +Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) + +Signature: + +```typescript +export declare type PublicLegacyAppInfo = Omit & { + legacy: true; +}; +``` diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 24c0e66359afa..300b09e17d15d 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -25,8 +25,8 @@ import { InternalApplicationStart, ApplicationStart, InternalApplicationSetup, - App, - LegacyApp, + PublicAppInfo, + PublicLegacyAppInfo, } from './types'; import { ApplicationServiceContract } from './test_types'; @@ -47,6 +47,7 @@ const createStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); return { + applications$: new BehaviorSubject>(new Map()), currentAppId$: currentAppId$.asObservable(), capabilities: capabilitiesServiceMock.createStartContract().capabilities, navigateToApp: jest.fn(), @@ -60,7 +61,7 @@ const createInternalStartContractMock = (): jest.Mocked(); return { - applications$: new BehaviorSubject>(new Map()), + applications$: new BehaviorSubject>(new Map()), capabilities: capabilitiesServiceMock.createStartContract().capabilities, currentAppId$: currentAppId$.asObservable(), getComponent: jest.fn(), diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index b65a8581e5b58..400d1881a5af8 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -34,7 +34,15 @@ import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; -import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types'; +import { + App, + PublicAppInfo, + AppNavLinkStatus, + AppStatus, + AppUpdater, + LegacyApp, + PublicLegacyAppInfo, +} from './types'; import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { @@ -366,7 +374,10 @@ describe('#setup()', () => { setup.registerAppUpdater(statusUpdater); const start = await service.start(startDeps); - let latestValue: ReadonlyMap = new Map(); + let latestValue: ReadonlyMap = new Map< + string, + PublicAppInfo | PublicLegacyAppInfo + >(); start.applications$.subscribe((apps) => { latestValue = apps; }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index b52b4984fb5e1..2224f72e2bd91 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -46,7 +46,7 @@ import { Mounter, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; -import { appendAppPath, parseAppUrl, relativeToAbsolute } from './utils'; +import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils'; interface SetupDeps { context: ContextSetup; @@ -291,7 +291,10 @@ export class ApplicationService { }; return { - applications$, + applications$: applications$.pipe( + map((apps) => new Map([...apps.entries()].map(([id, app]) => [id, getAppInfo(app)]))), + shareReplay(1) + ), capabilities, currentAppId$: this.currentAppId$.pipe( filter((appId) => appId !== undefined), diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index ec10d2bc22871..d51a4c0d69d42 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -39,7 +39,9 @@ export { AppLeaveAction, AppLeaveDefaultAction, AppLeaveConfirmAction, + LegacyApp, + PublicAppInfo, + PublicLegacyAppInfo, // Internal types InternalApplicationStart, - LegacyApp, } from './types'; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index c07d929fc5cea..2269fd0a4ca48 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -235,7 +235,7 @@ export interface App extends AppBase { appRoute?: string; } -/** @internal */ +/** @public */ export interface LegacyApp extends AppBase { appUrl: string; subUrlBase?: string; @@ -243,6 +243,24 @@ export interface LegacyApp extends AppBase { disableSubUrlTracking?: boolean; } +/** + * Public information about a registered {@link App | application} + * + * @public + */ +export type PublicAppInfo = Omit & { + legacy: false; +}; + +/** + * Information about a registered {@link LegacyApp | legacy application} + * + * @public + */ +export type PublicLegacyAppInfo = Omit & { + legacy: true; +}; + /** * A mount function called when the user navigates to this app's route. * @@ -649,6 +667,15 @@ export interface ApplicationStart { */ capabilities: RecursiveReadonly; + /** + * Observable emitting the list of currently registered apps and their associated status. + * + * @remarks + * Applications disabled by {@link Capabilities} will not be present in the map. Applications manually disabled from + * the client-side using an {@link AppUpdater | application updater} are present, with their status properly set as `inaccessible`. + */ + applications$: Observable>; + /** * Navigate to a given app * @@ -721,18 +748,7 @@ export interface ApplicationStart { } /** @internal */ -export interface InternalApplicationStart - extends Pick< - ApplicationStart, - 'capabilities' | 'navigateToApp' | 'navigateToUrl' | 'getUrlForApp' | 'currentAppId$' - > { - /** - * Apps available based on the current capabilities. - * Should be used to show navigation links and make routing decisions. - * Applications manually disabled from the client-side using {@link AppUpdater} - */ - applications$: Observable>; - +export interface InternalApplicationStart extends Omit { /** * Register a context provider for application mounting. Will only be available to applications that depend on the * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts index a86a1206fc983..b41945aa43682 100644 --- a/src/core/public/application/utils.test.ts +++ b/src/core/public/application/utils.test.ts @@ -17,7 +17,8 @@ * under the License. */ -import { LegacyApp, App } from './types'; +import { of } from 'rxjs'; +import { LegacyApp, App, AppStatus, AppNavLinkStatus } from './types'; import { BasePath } from '../http/base_path'; import { removeSlashes, @@ -25,6 +26,7 @@ import { isLegacyApp, relativeToAbsolute, parseAppUrl, + getAppInfo, } from './utils'; describe('removeSlashes', () => { @@ -459,3 +461,56 @@ describe('parseAppUrl', () => { }); }); }); + +describe('getAppInfo', () => { + const createApp = (props: Partial = {}): App => ({ + mount: () => () => undefined, + updater$: of(() => undefined), + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + appRoute: `/app/some-id`, + legacy: false, + ...props, + }); + + const createLegacyApp = (props: Partial = {}): LegacyApp => ({ + appUrl: '/my-app-url', + updater$: of(() => undefined), + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + legacy: true, + ...props, + }); + + it('converts an application and remove sensitive properties', () => { + const app = createApp(); + const info = getAppInfo(app); + + expect(info).toEqual({ + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + appRoute: `/app/some-id`, + legacy: false, + }); + }); + + it('converts a legacy application and remove sensitive properties', () => { + const app = createLegacyApp(); + const info = getAppInfo(app); + + expect(info).toEqual({ + appUrl: '/my-app-url', + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + legacy: true, + }); + }); +}); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts index 8987a9402f2db..1abd710548745 100644 --- a/src/core/public/application/utils.ts +++ b/src/core/public/application/utils.ts @@ -18,7 +18,7 @@ */ import { IBasePath } from '../http'; -import { App, LegacyApp } from './types'; +import { App, LegacyApp, PublicAppInfo, PublicLegacyAppInfo } from './types'; export interface AppUrlInfo { app: string; @@ -119,3 +119,19 @@ const removeBasePath = (url: string, basePath: IBasePath, origin: string): strin } return basePath.remove(url); }; + +export function getAppInfo(app: App | LegacyApp): PublicAppInfo | PublicLegacyAppInfo { + if (isLegacyApp(app)) { + const { updater$, ...infos } = app; + return { + ...infos, + legacy: true, + }; + } else { + const { updater$, mount, ...infos } = app; + return { + ...infos, + legacy: false, + }; + } +} diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0bc305ed9e28c..e39733cc10de7 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import * as Rx from 'rxjs'; import { take, toArray } from 'rxjs/operators'; -import { App } from '../application'; +import { App, PublicAppInfo } from '../application'; import { applicationServiceMock } from '../application/application_service.mock'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -29,6 +29,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { ChromeService } from './chrome_service'; +import { getAppInfo } from '../application/utils'; class FakeApp implements App { public title = `${this.id} App`; @@ -55,8 +56,8 @@ function defaultStartDeps(availableApps?: App[]) { }; if (availableApps) { - deps.application.applications$ = new Rx.BehaviorSubject>( - new Map(availableApps.map((app) => [app.id, app])) + deps.application.applications$ = new Rx.BehaviorSubject>( + new Map(availableApps.map((app) => [app.id, getAppInfo(app) as PublicAppInfo])) ); } diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 4c319873af804..ba04dbed49cd4 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -17,15 +17,12 @@ * under the License. */ -import { App, AppMount, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; +import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application'; import { toNavLink } from './to_nav_link'; import { httpServiceMock } from '../../mocks'; -function mount() {} - -const app = (props: Partial = {}): App => ({ - mount: (mount as unknown) as AppMount, +const app = (props: Partial = {}): PublicAppInfo => ({ id: 'some-id', title: 'some-title', status: AppStatus.accessible, @@ -35,7 +32,7 @@ const app = (props: Partial = {}): App => ({ ...props, }); -const legacyApp = (props: Partial = {}): LegacyApp => ({ +const legacyApp = (props: Partial = {}): PublicLegacyAppInfo => ({ appUrl: '/my-app-url', id: 'some-id', title: 'some-title', diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 24744fe53c82c..b8f97f9ddc005 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -17,12 +17,15 @@ * under the License. */ -import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; +import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; import { appendAppPath } from '../../application/utils'; -export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { +export function toNavLink( + app: PublicAppInfo | PublicLegacyAppInfo, + basePath: IBasePath +): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; const relativeBaseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) @@ -63,6 +66,6 @@ export function relativeToAbsolute(url: string) { return a.href; } -function isLegacyApp(app: App | LegacyApp): app is LegacyApp { +function isLegacyApp(app: PublicAppInfo | PublicLegacyAppInfo): app is PublicLegacyAppInfo { return app.legacy === true; } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 3698fdcfe9512..aa037329b1b6e 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -104,6 +104,7 @@ export { ApplicationSetup, ApplicationStart, App, + PublicAppInfo, AppBase, AppMount, AppMountDeprecated, @@ -120,6 +121,8 @@ export { AppUpdatableFields, AppUpdater, ScopedHistory, + LegacyApp, + PublicLegacyAppInfo, } from './application'; export { diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 810416cdbfe16..d77676b350f93 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -131,6 +131,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreStart = { ...core, application: { + applications$: core.application.applications$, currentAppId$: core.application.currentAppId$, capabilities: core.application.capabilities, getUrlForApp: core.application.getUrlForApp, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index c688373630a07..65c6b6ce4edba 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -135,6 +135,7 @@ export function createPluginStartContext< ): CoreStart { return { application: { + applications$: deps.application.applications$, currentAppId$: deps.application.currentAppId$, capabilities: deps.application.capabilities, navigateToApp: deps.application.navigateToApp, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 90c5dbb5f6558..bae0f9a2281cf 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -106,6 +106,7 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { + applications$: Observable>; capabilities: RecursiveReadonly; currentAppId$: Observable; getUrlForApp(appId: string, options?: { @@ -857,6 +858,18 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } +// @public (undocumented) +export interface LegacyApp extends AppBase { + // (undocumented) + appUrl: string; + // (undocumented) + disableSubUrlTracking?: boolean; + // (undocumented) + linkToLastSubUrl?: boolean; + // (undocumented) + subUrlBase?: string; +} + // @public @deprecated export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts @@ -993,6 +1006,16 @@ export interface PluginInitializerContext // @public (undocumented) export type PluginOpaqueId = symbol; +// @public +export type PublicAppInfo = Omit & { + legacy: false; +}; + +// @public +export type PublicLegacyAppInfo = Omit & { + legacy: true; +}; + // @public export type PublicUiSettingsParams = Omit; From d550131b9e4fdff8e43b51838cd70fa96e477de7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 2 Jun 2020 08:38:25 -0600 Subject: [PATCH 45/69] [Maps] fix mapbox glyphs error when EMS access is turned off (#67427) * [Maps] fix mapbox glyphs error when EMS access is turned off * santize range parameter * add api test * clean up mapbox view * add jest test for getGlyphsUrl * add license file * remove unneeded font files Co-authored-by: Elastic Machine --- src/dev/precommit_hook/casing_check_config.js | 2 + x-pack/plugins/maps/common/constants.ts | 1 + .../connected_components/map/mb/view.js | 5 +- x-pack/plugins/maps/public/meta.js | 23 ++++--- x-pack/plugins/maps/public/meta.test.js | 50 +++++++++++++- .../maps/server/fonts/open_sans/0-255.pbf | Bin 0 -> 74696 bytes .../maps/server/fonts/open_sans/1024-1279.pbf | Bin 0 -> 122545 bytes .../maps/server/fonts/open_sans/256-511.pbf | Bin 0 -> 66481 bytes .../maps/server/fonts/open_sans/768-1023.pbf | Bin 0 -> 33767 bytes .../maps/server/fonts/open_sans/8192-8447.pbf | Bin 0 -> 9663 bytes .../maps/server/fonts/open_sans/license.txt | 53 +++++++++++++++ x-pack/plugins/maps/server/routes.js | 62 ++++++++++++++---- .../api_integration/apis/maps/fonts_api.js | 21 ++++++ .../test/api_integration/apis/maps/index.js | 1 + 14 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/maps/server/fonts/open_sans/0-255.pbf create mode 100644 x-pack/plugins/maps/server/fonts/open_sans/1024-1279.pbf create mode 100644 x-pack/plugins/maps/server/fonts/open_sans/256-511.pbf create mode 100644 x-pack/plugins/maps/server/fonts/open_sans/768-1023.pbf create mode 100644 x-pack/plugins/maps/server/fonts/open_sans/8192-8447.pbf create mode 100644 x-pack/plugins/maps/server/fonts/open_sans/license.txt create mode 100644 x-pack/test/api_integration/apis/maps/fonts_api.js diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 2eee3b2c53bd3..d968a365e7bb9 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -64,6 +64,8 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/plugins/apm/public/**/*', 'x-pack/plugins/apm/scripts/**/*', 'x-pack/plugins/apm/e2e/**/*', + + 'x-pack/plugins/maps/server/fonts/**/*', ]; /** diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 8fa44c512df4b..d357f11f5e3e1 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -30,6 +30,7 @@ export const TELEMETRY_TYPE = 'maps-telemetry'; export const MAP_APP_PATH = `app/${APP_ID}`; export const GIS_API_PATH = `api/${APP_ID}`; export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; +export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; export const MAP_BASE_URL = `/${MAP_APP_PATH}#/${MAP_SAVED_OBJECT_TYPE}`; diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index c4b28e33747ee..42235bfd5442e 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -118,11 +118,8 @@ export class MBMapContainer extends React.Component { version: 8, sources: {}, layers: [], + glyphs: getGlyphUrl(), }; - const glyphUrl = getGlyphUrl(); - if (glyphUrl) { - mbStyle.glyphs = glyphUrl; - } const options = { attributionControl: false, diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index 3ffd0578796ce..46c5e5cda3617 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -5,15 +5,7 @@ */ import { - GIS_API_PATH, - EMS_FILES_CATALOGUE_PATH, - EMS_TILES_CATALOGUE_PATH, - EMS_GLYPHS_PATH, - EMS_APP_NAME, -} from '../common/constants'; -import { i18n } from '@kbn/i18n'; -import { EMSClient } from '@elastic/ems-client'; -import { + getHttp, getLicenseId, getIsEmsEnabled, getRegionmapLayers, @@ -25,6 +17,17 @@ import { getProxyElasticMapsServiceInMaps, getKibanaVersion, } from './kibana_services'; +import { + GIS_API_PATH, + EMS_FILES_CATALOGUE_PATH, + EMS_TILES_CATALOGUE_PATH, + EMS_GLYPHS_PATH, + EMS_APP_NAME, + FONTS_API_PATH, +} from '../common/constants'; +import { i18n } from '@kbn/i18n'; +import { EMSClient } from '@elastic/ems-client'; + import fetch from 'node-fetch'; const GIS_API_RELATIVE = `../${GIS_API_PATH}`; @@ -95,7 +98,7 @@ export function getEMSClient() { export function getGlyphUrl() { if (!getIsEmsEnabled()) { - return ''; + return getHttp().basePath.prepend(`/${FONTS_API_PATH}/{fontstack}/{range}`); } return getProxyElasticMapsServiceInMaps() ? relativeToAbsolute(`../${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}/${EMS_GLYPHS_PATH}`) + diff --git a/x-pack/plugins/maps/public/meta.test.js b/x-pack/plugins/maps/public/meta.test.js index 24dc65e9fc71c..5c04a57c00058 100644 --- a/x-pack/plugins/maps/public/meta.test.js +++ b/x-pack/plugins/maps/public/meta.test.js @@ -5,7 +5,7 @@ */ import { EMSClient } from '@elastic/ems-client'; -import { getEMSClient } from './meta'; +import { getEMSClient, getGlyphUrl } from './meta'; jest.mock('@elastic/ems-client'); @@ -22,10 +22,56 @@ describe('default use without proxy', () => { require('./kibana_services').getEmsLandingPageUrl = () => 'http://test.com'; }); - it('should construct EMSClient with absolute file and tile API urls', async () => { + test('should construct EMSClient with absolute file and tile API urls', async () => { getEMSClient(); const mockEmsClientCall = EMSClient.mock.calls[0]; expect(mockEmsClientCall[0].fileApiUrl.startsWith('https://file-api')).toBe(true); expect(mockEmsClientCall[0].tileApiUrl.startsWith('https://tile-api')).toBe(true); }); }); + +describe('getGlyphUrl', () => { + describe('EMS enabled', () => { + const EMS_FONTS_URL_MOCK = 'ems/fonts'; + beforeAll(() => { + require('./kibana_services').getIsEmsEnabled = () => true; + require('./kibana_services').getEmsFontLibraryUrl = () => EMS_FONTS_URL_MOCK; + }); + + describe('EMS proxy enabled', () => { + beforeAll(() => { + require('./kibana_services').getProxyElasticMapsServiceInMaps = () => true; + }); + + test('should return proxied EMS fonts URL', async () => { + expect(getGlyphUrl()).toBe('http://localhost/api/maps/ems/tiles/fonts/{fontstack}/{range}'); + }); + }); + + describe('EMS proxy disabled', () => { + beforeAll(() => { + require('./kibana_services').getProxyElasticMapsServiceInMaps = () => false; + }); + + test('should return EMS fonts URL', async () => { + expect(getGlyphUrl()).toBe(EMS_FONTS_URL_MOCK); + }); + }); + }); + + describe('EMS disabled', () => { + beforeAll(() => { + const mockHttp = { + basePath: { + prepend: (path) => `abc${path}`, + }, + }; + require('./kibana_services').getHttp = () => mockHttp; + require('./kibana_services').getIsEmsEnabled = () => false; + }); + + test('should return kibana fonts URL', async () => { + expect(getGlyphUrl()).toBe('abc/api/maps/fonts/{fontstack}/{range}'); + }); + }); +}); diff --git a/x-pack/plugins/maps/server/fonts/open_sans/0-255.pbf b/x-pack/plugins/maps/server/fonts/open_sans/0-255.pbf new file mode 100644 index 0000000000000000000000000000000000000000..ab811ae10a2e7bd58ab67fd120cc313a03e3bc11 GIT binary patch literal 74696 zcmeFaWpGGFG4eoT(={Ozeq|;$$a2z|1A%>Wl8D+~d$;`}bnPifgNw#HXX3HRR zlJ~y%-m3X(W`4}fpQ)NZGtWMfobiQ+xb=rN}^H*g zTb6eFi$g6LsZ9$zeMQ0M?kSD)LViJ@sasP0>{d^HfQf5j-ONT;9?Qr%zGiBjm+NQf z6jwdD){*U_?+{ZtzS^GUt!E$29b0bAVCvXLmJbXxr+aFAqh@Lgh(U#X;TX4NX`v~N;m_{g94T*Fm}^L;`{i~D z221K^r)v{ky)&D;It#11x+-IxoFlXIa^utT@>2aR^j&;eEDtXh%ga{%(ht{vpq}uj z%ZDeVZb`44pwVbo<&4QZ_&T!!16j_xuP)MPKR?uTBU6$Qo`Yu&vsbxE`~K{6L*E?C zPIYPnpWl$;p?Uw@ttW5H$)k-|Ww)=+)F;zF-g~U1YiJu$HncxjTF|pKRvGK4E~9Gg z>E{<$J+akOP&&9bP!jIrq-W}voSU25IJerKTQ$Dfou8W(?r0xX&O=+7$H(jnJ{r@R zWVY7V*LOGYv9-0nzS5TI1=<+eF8SCR#|LQEA$eRbcTC9VatHSZaaa;hcMXk;ORAq) z?a09`dJ5uVBE8Ln^Qx<>TbJgWsvG83+N-O%sZ2f0z~toQ`V>A^=J7$+4QOj?YoG9M zZJ&G)S~#bpqhoZtx1(cVcL;~g*}jHe3EA0o(}K?2%CRkeVRm+Aq>F~RyH9${;$m|; zb#GrEZ)g2C3PxnErt31vy;B+bdg?MaWG#Y=dIh6gc9&ohOHB32y~{s=R@l+mS)A9_ z*;yKC_x?{Q8tu6rBQ7T=Cm|*~Cnw3rLy+k3r*yD8HL5kO#Pt3-mMiL z=_dq-dRr;^WVS4Pn(rsyNBh}pzjlc&@15TlZcX!>%hE%fG|hZd%i2fgg`YR)`s=cT ztUp-$#Id>f{nFmXa807SsjDYFkUVeO*zQV4t{*clJwB9r{`M8ffxrk(OI2|$xgT$B zy_@Y1jvwpmM89|O^K?Th6aD>d!OF&(aBGIgiLj3<;|)*EEePf&dYd`%Ohex!c3DjW z`F(L_G}BDck`WjgPhCI3$Jt2nzM_t~omXbt$VhF1i<<21vp1i}esqf~E-zp9BA$d3*YD|94V%q?^9G{#M`KJ2WDU=nU<(9z8s*xU3{Q#@oz*nV4Ttkmhgx_A*V)F+9JXKRnn{n&4?4Uev|sS42A~ zU85QMW;9N$Z*0%;3d4NT8>TkfgfVTkUtJ+|@FbyE4)2asak<5*A�)=*d|8?b+lyoy!YtJ!4}%by-1H?;T^f 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 Zcu8yF`^@02wfGfSi!Wd;wqN+@zXQQuMRx!I literal 0 HcmV?d00001 diff --git a/x-pack/plugins/maps/server/fonts/open_sans/1024-1279.pbf b/x-pack/plugins/maps/server/fonts/open_sans/1024-1279.pbf new file mode 100644 index 0000000000000000000000000000000000000000..7cda8da1d0388a7d800e57f25a5a2844722cf6ed GIT binary patch literal 122545 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 LW<3p12tWEa8bYCv literal 0 HcmV?d00001 diff --git a/x-pack/plugins/maps/server/fonts/open_sans/256-511.pbf b/x-pack/plugins/maps/server/fonts/open_sans/256-511.pbf new file mode 100644 index 0000000000000000000000000000000000000000..6e108e53a26f8579a46c9626a5e312c95c8ed83f GIT binary patch literal 66481 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 JiG}#ce*j4AU_Af; literal 0 HcmV?d00001 diff --git a/x-pack/plugins/maps/server/fonts/open_sans/768-1023.pbf b/x-pack/plugins/maps/server/fonts/open_sans/768-1023.pbf new file mode 100644 index 0000000000000000000000000000000000000000..a3efbb9361d4d603042de3f2bdb4e261b8170763 GIT binary patch literal 33767 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/maps/server/fonts/open_sans/license.txt b/x-pack/plugins/maps/server/fonts/open_sans/license.txt new file mode 100644 index 0000000000000..7783de532a331 --- /dev/null +++ b/x-pack/plugins/maps/server/fonts/open_sans/license.txt @@ -0,0 +1,53 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 63895ea8b9822..ad66712eb3ad6 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -21,12 +21,15 @@ import { GIS_API_PATH, EMS_SPRITES_PATH, INDEX_SETTINGS_API_PATH, + FONTS_API_PATH, } from '../common/constants'; import { EMSClient } from '@elastic/ems-client'; import fetch from 'node-fetch'; import { i18n } from '@kbn/i18n'; import { getIndexPatternSettings } from './lib/get_index_pattern_settings'; import { schema } from '@kbn/config-schema'; +import fs from 'fs'; +import path from 'path'; const ROOT = `/${GIS_API_PATH}`; @@ -76,7 +79,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -108,7 +111,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, validate: false, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -144,7 +147,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { path: `${ROOT}/${EMS_CATALOGUE_PATH}`, validate: false, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -180,7 +183,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, validate: false, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -210,7 +213,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, validate: false, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -258,7 +261,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -294,7 +297,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -344,7 +347,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -386,7 +389,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -423,7 +426,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, validate: false, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -444,7 +447,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, { ok, badRequest }) => { + async (context, request, { ok, badRequest }) => { if (!checkEMSProxyEnabled()) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } @@ -481,6 +484,39 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { } ); + router.get( + { + path: `/${FONTS_API_PATH}/{fontstack}/{range}`, + validate: { + params: schema.object({ + fontstack: schema.string(), + range: schema.string(), + }), + }, + }, + (context, request, response) => { + return new Promise((resolve, reject) => { + const santizedRange = path.normalize(request.params.range); + const fontPath = path.join(__dirname, 'fonts', 'open_sans', `${santizedRange}.pbf`); + fs.readFile(fontPath, (error, data) => { + if (error) { + reject( + response.custom({ + statusCode: 404, + }) + ); + } else { + resolve( + response.ok({ + body: data, + }) + ); + } + }); + }); + } + ); + router.get( { path: `/${INDEX_SETTINGS_API_PATH}`, @@ -490,7 +526,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { }), }, }, - async (con, request, response) => { + async (context, request, response) => { const { query } = request; if (!query.indexPatternTitle) { @@ -502,7 +538,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { } try { - const resp = await con.core.elasticsearch.legacy.client.callAsCurrentUser( + const resp = await context.core.elasticsearch.legacy.client.callAsCurrentUser( 'indices.getSettings', { index: query.indexPatternTitle, diff --git a/x-pack/test/api_integration/apis/maps/fonts_api.js b/x-pack/test/api_integration/apis/maps/fonts_api.js new file mode 100644 index 0000000000000..d367fb6a0610b --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/fonts_api.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('fonts', () => { + it('should return fonts', async () => { + const resp = await supertest + .get(`/api/maps/fonts/Open%20Sans%20Regular,Arial%20Unicode%20MS%20Regular/0-255`) + .expect(200); + + expect(resp.body.length).to.be(74696); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index a6267b9fd0cea..f9dff19229645 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -13,6 +13,7 @@ export default function ({ loadTestFile, getService }) { }); describe('', () => { + loadTestFile(require.resolve('./fonts_api')); loadTestFile(require.resolve('./index_settings')); loadTestFile(require.resolve('./migrations')); }); From a1a1d5d2f79ff781d85e0bb7d845f1f7094527aa Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 2 Jun 2020 10:41:04 -0400 Subject: [PATCH 46/69] Ensure we query for more than 10 (#67172) --- .../lib/setup/collection/get_collection_status.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index fcb54e92f649c..607503673276b 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -18,7 +18,7 @@ import { getLivesNodes } from '../../elasticsearch/nodes/get_nodes/get_live_node const NUMBER_OF_SECONDS_AGO_TO_LOOK = 30; -const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nodeUuid) => { +const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nodeUuid, size) => { const start = get(req.payload, 'timeRange.min') || `now-${NUMBER_OF_SECONDS_AGO_TO_LOOK}s`; const end = get(req.payload, 'timeRange.max') || 'now'; @@ -73,6 +73,7 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod es_uuids: { terms: { field: 'node_stats.node_id', + size, }, aggs: { by_timestamp: { @@ -85,6 +86,7 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod kibana_uuids: { terms: { field: 'kibana_stats.kibana.uuid', + size, }, aggs: { by_timestamp: { @@ -97,6 +99,7 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod beats_uuids: { terms: { field: 'beats_stats.beat.uuid', + size, }, aggs: { by_timestamp: { @@ -107,11 +110,13 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod beat_type: { terms: { field: 'beats_stats.beat.type', + size, }, }, cluster_uuid: { terms: { field: 'cluster_uuid', + size, }, }, }, @@ -119,6 +124,7 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod logstash_uuids: { terms: { field: 'logstash_stats.logstash.uuid', + size, }, aggs: { by_timestamp: { @@ -129,6 +135,7 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod cluster_uuid: { terms: { field: 'cluster_uuid', + size, }, }, }, @@ -348,6 +355,7 @@ export const getCollectionStatus = async ( ) => { const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); + const size = config.get('monitoring.ui.max_bucket_size'); const hasPermissions = await hasNecessaryPermissions(req); if (!hasPermissions) { @@ -369,7 +377,7 @@ export const getCollectionStatus = async ( ]; const [recentDocuments, detectedProducts] = await Promise.all([ - await getRecentMonitoringDocuments(req, indexPatterns, clusterUuid, nodeUuid), + await getRecentMonitoringDocuments(req, indexPatterns, clusterUuid, nodeUuid, size), await detectProducts(req, isLiveCluster), ]); From 52c518a6aed5a46ed2d25152a70833898c134e67 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Tue, 2 Jun 2020 08:57:11 -0600 Subject: [PATCH 47/69] [SIEM] Fixes column drag and drop in timeline-based views (#67799) ## Summary Fixes a bug in timeline-based views, (e.g. the Host Events table), where a column would revert back to it's original position after being dragged and dropped to a new position. Only timeline-based views were effected, not the timeline itself. To reproduce: 1) On the SIEM Overview page, click the `View events` button 2) Drag and drop any column in the Events table to a new position **Expected Result** - The column is relocated to the position where it was dropped **Actual Result** - The column reverts to it's original position ## Testing - This PR adds a Cypress test for this scenario - The new test was successfully run (at least) 10 times via `node x-pack/plugins/siem/scripts/loop_cypress_tests.js` - This fix was desk tested in: - Chrome `83.0.4103.61` - Firefox `76.0.1` - Safari `13.1` --- .../cypress/integration/events_viewer.spec.ts | 26 +++++++++++++++++++ .../plugins/siem/cypress/screens/timeline.ts | 5 ++++ .../siem/cypress/tasks/hosts/events.ts | 23 ++++++++++++++++ .../drag_drop_context_wrapper.tsx | 3 ++- .../components/drag_and_drop/helpers.test.ts | 13 ++++++++++ .../components/drag_and_drop/helpers.ts | 3 +++ 6 files changed, 72 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts index 26ebaeb844825..82b4f4f0fbe34 100644 --- a/x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts @@ -17,6 +17,7 @@ import { LOAD_MORE, LOCAL_EVENTS_COUNT, } from '../screens/hosts/events'; +import { HEADERS_GROUP } from '../screens/timeline'; import { closeFieldsBrowser, filterFieldsBrowser } from '../tasks/fields_browser'; import { loginAndWaitForPage } from '../tasks/login'; @@ -25,6 +26,7 @@ import { addsHostGeoCityNameToHeader, addsHostGeoCountryNameToHeader, closeModal, + dragAndDropColumn, openEventsViewerFieldsBrowser, opensInspectQueryModal, resetFields, @@ -150,4 +152,28 @@ describe('Events Viewer', () => { cy.get(LOCAL_EVENTS_COUNT).invoke('text').should('not.equal', defaultNumberOfLoadedEvents); }); }); + + context.skip('Events columns', () => { + before(() => { + loginAndWaitForPage(HOSTS_PAGE); + openEvents(); + waitsForEventsToBeLoaded(); + }); + + afterEach(() => { + openEventsViewerFieldsBrowser(); + resetFields(); + }); + + it('re-orders columns via drag and drop', () => { + const originalColumnOrder = + '@timestampmessagehost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; + const expectedOrderAfterDragAndDrop = + 'message@timestamphost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; + + cy.get(HEADERS_GROUP).invoke('text').should('equal', originalColumnOrder); + dragAndDropColumn({ column: 0, newPosition: 1 }); + cy.get(HEADERS_GROUP).invoke('text').should('equal', expectedOrderAfterDragAndDrop); + }); + }); }); diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index ed1dc97454fb3..bb232b752994a 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -8,6 +8,11 @@ export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; +export const DRAGGABLE_HEADER = + '[data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; + +export const HEADERS_GROUP = '[data-test-subj="headers-group"]'; + export const ID_HEADER_FIELD = '[data-test-subj="timeline"] [data-test-subj="header-text-_id"]'; export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name-_id"]'; diff --git a/x-pack/plugins/siem/cypress/tasks/hosts/events.ts b/x-pack/plugins/siem/cypress/tasks/hosts/events.ts index dff58b4b0e9ea..a593650989259 100644 --- a/x-pack/plugins/siem/cypress/tasks/hosts/events.ts +++ b/x-pack/plugins/siem/cypress/tasks/hosts/events.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { drag, drop } from '../common'; import { CLOSE_MODAL, EVENTS_VIEWER_FIELDS_BUTTON, @@ -15,6 +16,7 @@ import { RESET_FIELDS, SERVER_SIDE_EVENT_COUNT, } from '../../screens/hosts/events'; +import { DRAGGABLE_HEADER } from '../../screens/timeline'; export const addsHostGeoCityNameToHeader = () => { cy.get(HOST_GEO_CITY_NAME_CHECKBOX).check({ @@ -58,3 +60,24 @@ export const resetFields = () => { export const waitsForEventsToBeLoaded = () => { cy.get(SERVER_SIDE_EVENT_COUNT).should('exist').invoke('text').should('not.equal', '0'); }; + +export const dragAndDropColumn = ({ + column, + newPosition, +}: { + column: number; + newPosition: number; +}) => { + cy.get(DRAGGABLE_HEADER).first().should('exist'); + cy.get(DRAGGABLE_HEADER) + .eq(column) + .then((header) => drag(header)); + + cy.wait(3000); // wait for DOM updates before moving + + cy.get(DRAGGABLE_HEADER) + .eq(newPosition) + .then((targetPosition) => { + drop(targetPosition); + }); +}; diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 3bd2a3da1c88b..c71cd8e60596c 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -27,6 +27,7 @@ import { addFieldToTimelineColumns, addProviderToTimeline, fieldWasDroppedOnTimelineColumns, + getTimelineIdFromColumnDroppableId, IS_DRAGGING_CLASS_NAME, IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, providerWasDroppedOnTimeline, @@ -82,7 +83,7 @@ const onDragEndHandler = ({ browserFields, dispatch, result, - timelineId: ACTIVE_TIMELINE_REDUX_ID, + timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''), }); } }; diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts index 69fbedb6462cb..be58381fbca1b 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts @@ -32,6 +32,7 @@ import { getDroppableId, getFieldIdFromDraggable, getProviderIdFromDraggable, + getTimelineIdFromColumnDroppableId, getTimelineProviderDraggableId, getTimelineProviderDroppableId, providerWasDroppedOnTimeline, @@ -984,4 +985,16 @@ describe('helpers', () => { }); }); }); + + describe('getTimelineIdFromColumnDroppableId', () => { + test('it returns the expected timelineId from a column droppableId', () => { + expect(getTimelineIdFromColumnDroppableId(DROPPABLE_ID_TIMELINE_COLUMNS)).toEqual( + 'timeline-1' + ); + }); + + test('it returns an empty string when the droppableId is an empty string', () => { + expect(getTimelineIdFromColumnDroppableId('')).toEqual(''); + }); + }); }); diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts index ad370f647738f..0037fc3cae628 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts @@ -332,3 +332,6 @@ export const allowTopN = ({ return isWhitelistedNonBrowserField || (isAggregatable && isAllowedType); }; + +export const getTimelineIdFromColumnDroppableId = (droppableId: string) => + droppableId.slice(droppableId.lastIndexOf('.') + 1); From ce7940adc28ba53627bfa401a97b8bf4af0aa901 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 2 Jun 2020 11:31:30 -0400 Subject: [PATCH 48/69] Allow functions to return falsy values (#67796) Co-authored-by: Elastic Machine --- .../create_streaming_batched_function.test.ts | 41 +++++++++++++++++++ .../create_streaming_batched_function.ts | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts index 26c1d9e5033e0..da6c940c48d0a 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts @@ -303,6 +303,47 @@ describe('createStreamingBatchedFunction()', () => { expect(await promise3).toEqual({ foo: 'bar 2' }); }); + test('resolves falsy results', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = fn({ a: '1' }); + const promise2 = fn({ b: '2' }); + const promise3 = fn({ c: '3' }); + await new Promise((r) => setTimeout(r, 6)); + + stream.next( + JSON.stringify({ + id: 0, + result: false, + }) + '\n' + ); + stream.next( + JSON.stringify({ + id: 1, + result: 0, + }) + '\n' + ); + stream.next( + JSON.stringify({ + id: 2, + result: '', + }) + '\n' + ); + + expect(await isPending(promise1)).toBe(false); + expect(await isPending(promise2)).toBe(false); + expect(await isPending(promise3)).toBe(false); + expect(await promise1).toEqual(false); + expect(await promise2).toEqual(0); + expect(await promise3).toEqual(''); + }); + test('rejects promise on error response', async () => { const { fetchStreaming, stream } = setup(); const fn = createStreamingBatchedFunction({ diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts index f80a97137d1ab..89793fff6b325 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts @@ -106,7 +106,7 @@ export const createStreamingBatchedFunction = ( if (response.error) { responsesReceived++; items[response.id].future.reject(response.error); - } else if (response.result) { + } else if (response.result !== undefined) { responsesReceived++; items[response.id].future.resolve(response.result); } From 77e7e0bb49f38cd008e96d1e1c808e21f0732c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Tue, 2 Jun 2020 17:36:47 +0200 Subject: [PATCH 49/69] [Logs UI] Ensure live stream always gets latest entries (#67935) LogPositionState doesn't always reevaluate the value of `endTimestamp` when live stream is on. The [dependencies][1] for it to change rely on the scroll position to update. If there's less than one scroll page or the previous API call didn't return any entries, the `endTimestamp` would not update. We force `Date.now()` as an `endTimestamp` on the API call to ensure it always gets the latest entries possible, regardless of the state. This introduces some inconsistency that will be fixed once work beings on #65493. [1]: https://github.com/elastic/kibana/blob/fe4c164681e92ef5bf0c28f7ab3dfe00a5aacd6f/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts#L160-L173 --- .../containers/logs/log_entries/index.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts index a6d66d47975c0..5fe9a45a7ceed 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts @@ -194,7 +194,10 @@ const useFetchEntriesEffect = ( } }; - const runFetchMoreEntriesRequest = async (direction: ShouldFetchMoreEntries) => { + const runFetchMoreEntriesRequest = async ( + direction: ShouldFetchMoreEntries, + overrides: Partial = {} + ) => { if (!props.startTimestamp || !props.endTimestamp) { return; } @@ -209,10 +212,10 @@ const useFetchEntriesEffect = ( try { const commonFetchArgs: LogEntriesBaseRequest = { - sourceId: props.sourceId, - startTimestamp: props.startTimestamp, - endTimestamp: props.endTimestamp, - query: props.filterQuery, + sourceId: overrides.sourceId || props.sourceId, + startTimestamp: overrides.startTimestamp || props.startTimestamp, + endTimestamp: overrides.endTimestamp || props.endTimestamp, + query: overrides.filterQuery || props.filterQuery, }; const fetchArgs: LogEntriesRequest = getEntriesBefore @@ -279,10 +282,10 @@ const useFetchEntriesEffect = ( const streamEntriesEffect = () => { (async () => { if (props.isStreaming && !state.isLoadingMore && !state.isReloading) { + const endTimestamp = Date.now(); if (startedStreaming) { await new Promise((res) => setTimeout(res, LIVE_STREAM_INTERVAL)); } else { - const endTimestamp = Date.now(); props.jumpToTargetPosition({ tiebreaker: 0, time: endTimestamp }); setStartedStreaming(true); if (state.hasMoreAfterEnd) { @@ -290,7 +293,9 @@ const useFetchEntriesEffect = ( return; } } - const newEntriesEnd = await runFetchMoreEntriesRequest(ShouldFetchMoreEntries.After); + const newEntriesEnd = await runFetchMoreEntriesRequest(ShouldFetchMoreEntries.After, { + endTimestamp, + }); if (newEntriesEnd) { props.jumpToTargetPosition(newEntriesEnd); } From 13c24b0d818dd4d67073746236064e07c7546493 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 2 Jun 2020 18:43:13 +0200 Subject: [PATCH 50/69] [Uptime] Fix ping io ts type (#66926) Co-authored-by: Elastic Machine --- x-pack/plugins/uptime/common/runtime_types/ping/ping.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index d8dc7fc89d94b..ab539b38c3e41 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -36,9 +36,9 @@ export const MonitorType = t.intersection([ check_group: t.string, ip: t.string, name: t.string, - timespan: t.partial({ + timespan: t.type({ gte: t.string, - lte: t.string, + lt: t.string, }), }), ]); @@ -55,13 +55,13 @@ export const PingType = t.intersection([ agent: t.intersection([ t.type({ ephemeral_id: t.string, - hostname: t.string, id: t.string, type: t.string, version: t.string, }), t.partial({ name: t.string, + hostname: t.string, }), ]), container: t.partial({ From 332a92d3d537c87f6b8623a36520deb4f7c78953 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 2 Jun 2020 11:56:55 -0500 Subject: [PATCH 51/69] [SIEM][Detections] Allow synchronous rule actions to be updated via PATCH (#67914) * Update synchronous actions in patchRules This method was typed to accept actions, but it was not doing anything with them. This was mainly a "bug by omission" so I'm simply adding unit tests for regression purposes. * Allow synchronous actions to be patched either individually or in bulk Now that patchRules uses this field, we simply need to pass it. Co-authored-by: Garrett Spong Co-authored-by: Elastic Machine --- .../routes/rules/patch_rules_bulk_route.ts | 1 + .../routes/rules/patch_rules_route.ts | 1 + .../rules/patch_rules.test.ts | 78 +++++++++++++++++++ .../lib/detection_engine/rules/patch_rules.ts | 4 +- 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 0d0cd28738c92..e9c0ca08c88ee 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -122,6 +122,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => version, anomalyThreshold, machineLearningJobId, + actions, }); if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index ae23e0efc857d..2a1ac9862e7d0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -118,6 +118,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { version, anomalyThreshold, machineLearningJobId, + actions, }); if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts index 3c1267c939345..0dffa665f780b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts @@ -91,4 +91,82 @@ describe('patchRules', () => { }) ); }); + + describe('regression tests', () => { + it("updates the rule's actions if provided", async () => { + const existingRule = getResult(); + + const action = { + action_type_id: '.slack', + id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', + }, + group: 'default', + }; + + await patchRules({ + alertsClient, + savedObjectsClient, + actions: [action], + rule: existingRule, + }); + + expect(alertsClient.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + actions: [ + { + actionTypeId: '.slack', + id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', + }, + group: 'default', + }, + ], + }), + }) + ); + }); + + it('does not update actions if none are specified', async () => { + const existingRule = { + ...getResult(), + actions: [ + { + actionTypeId: '.slack', + id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', + }, + group: 'default', + }, + ], + }; + + await patchRules({ + alertsClient, + savedObjectsClient, + rule: existingRule, + }); + + expect(alertsClient.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + actions: [ + { + actionTypeId: '.slack', + id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', + }, + group: 'default', + }, + ], + }), + }) + ); + }); + }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 1e728ae7b8d0b..950a3e90fb70c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -6,6 +6,7 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerts/server'; +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; import { calculateVersion, calculateName, calculateInterval } from './utils'; @@ -44,6 +45,7 @@ export const patchRules = async ({ exceptions_list, anomalyThreshold, machineLearningJobId, + actions, }: PatchRuleParams): Promise => { if (rule == null) { return null; @@ -121,7 +123,7 @@ export const patchRules = async ({ schedule: { interval: calculateInterval(interval, rule.schedule.interval), }, - actions: rule.actions, + actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, params: nextParams, }, }); From 92237517be20a23265b82c14fe104a4123e9409e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 2 Jun 2020 20:01:39 +0300 Subject: [PATCH 52/69] [SIEM] Update cypress to 4.5.0 (#67961) --- x-pack/package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index dc23602bac86c..9f69cbc40bf33 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -128,7 +128,7 @@ "cheerio": "0.22.0", "commander": "3.0.2", "copy-webpack-plugin": "^5.0.4", - "cypress": "^4.4.1", + "cypress": "4.5.0", "cypress-multi-reporters": "^1.2.3", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", diff --git a/yarn.lock b/yarn.lock index 5d47056857bbf..5506165da2d34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4865,9 +4865,9 @@ "@types/sinon" "*" "@types/sinon@*": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.0.tgz#5b70a360f55645dd64f205defd2a31b749a59799" - integrity sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA== + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== dependencies: "@types/sinonjs__fake-timers" "*" @@ -10532,10 +10532,10 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.4.1.tgz#f5aa1aa5f328f1299bff328103f7cbad89e80f29" - integrity sha512-LcskZ/PXRG9XTlEeeenKqz/KddT1x+7O7dqXsdKWPII01LxLNmNHIvHnlUqApchVbinJ5vir6J255CkELSeL0A== +cypress@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" + integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/request" "2.88.5" From 6481402510a9ba4e2f66d1a3da398b51797595c4 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 2 Jun 2020 11:41:23 -0700 Subject: [PATCH 53/69] delete flaky suite with flaky beforeAll hook (#67554) (#67555) (#67556) --- .../integration_tests/ingest_coverage.test.js | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js deleted file mode 100644 index 03126d130e984..0000000000000 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import execa from 'execa'; -import expect from '@kbn/expect'; - -const ROOT_DIR = resolve(__dirname, '../../../../..'); -const MOCKS_DIR = resolve(__dirname, './mocks'); -const env = { - BUILD_ID: 407, - CI_RUN_URL: 'https://kibana-ci.elastic.co/job/elastic+kibana+code-coverage/407/', - STATIC_SITE_URL_BASE: 'https://kibana-coverage.elastic.dev', - TIME_STAMP: '2020-03-02T21:11:47Z', - ES_HOST: 'https://super:changeme@some.fake.host:9243', - NODE_ENV: 'integration_test', - COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', -}; -const verboseArgs = [ - 'scripts/ingest_coverage.js', - '--verbose', - '--vcsInfoPath', - 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', - '--path', -]; - -// FLAKY: https://github.com/elastic/kibana/issues/67554 -// FLAKY: https://github.com/elastic/kibana/issues/67555 -// FLAKY: https://github.com/elastic/kibana/issues/67556 -describe.skip('Ingesting coverage', () => { - const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const siteUrlRegex = /"staticSiteUrl": (".+",)/; - let actualUrl = ''; - - beforeAll(async () => { - const opts = [...verboseArgs, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - actualUrl = siteUrlRegex.exec(stdout)[1]; - }); - - describe(`staticSiteUrl`, () => { - it('should contain the static host', () => { - const staticHost = /https:\/\/kibana-coverage\.elastic\.dev/; - expect(staticHost.test(actualUrl)).ok(); - }); - it('should contain the timestamp', () => { - const timeStamp = /\d{4}-\d{2}-\d{2}T\d{2}.*\d{2}.*\d{2}Z/; - expect(timeStamp.test(actualUrl)).ok(); - }); - it('should contain the folder structure', () => { - const folderStructure = /(?:.*|.*-combined)\//; - expect(folderStructure.test(actualUrl)).ok(); - }); - }); -}); From 9c5eb3a37bc3b8c865d19296c6199085a7178645 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Tue, 2 Jun 2020 15:20:05 -0400 Subject: [PATCH 54/69] [Endpoint]EMT-395: add policy configuration and reenable tests (#67967) [Endpoint]EMT-395: add policy configuration and tests --- .../siem/common/endpoint/generate_data.ts | 4 ---- x-pack/plugins/siem/common/endpoint/types.ts | 4 ---- .../scripts/endpoint/resolver_generator.ts | 2 +- .../server/endpoint/alerts/index_pattern.ts | 5 +++-- .../apis/endpoint/alerts/index.ts | 2 +- .../apis/endpoint/alerts/index_pattern.ts | 9 +++++++-- .../api_integration/apis/endpoint/metadata.ts | 2 +- .../alerts/host_api_feature/data.json.gz | Bin 849 -> 855 bytes .../alerts/host_api_feature/mappings.json | 2 +- .../endpoint/metadata/api_feature/data.json | 18 +++++++++--------- .../metadata/api_feature/mappings.json | 2 +- 11 files changed, 24 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/siem/common/endpoint/generate_data.ts b/x-pack/plugins/siem/common/endpoint/generate_data.ts index a683db86dc6a0..597ad4df64dfe 100644 --- a/x-pack/plugins/siem/common/endpoint/generate_data.ts +++ b/x-pack/plugins/siem/common/endpoint/generate_data.ts @@ -845,10 +845,6 @@ export class EndpointDocGenerator { }, ], id: this.commonInfo.endpoint.policy.id, - policy: { - id: this.commonInfo.endpoint.policy.id, - version: policyVersion, - }, response: { configurations: { events: { diff --git a/x-pack/plugins/siem/common/endpoint/types.ts b/x-pack/plugins/siem/common/endpoint/types.ts index 6d04f1dfac38f..45b5cf2526e12 100644 --- a/x-pack/plugins/siem/common/endpoint/types.ts +++ b/x-pack/plugins/siem/common/endpoint/types.ts @@ -685,10 +685,6 @@ export interface HostPolicyResponse { id: string; status: HostPolicyResponseActionStatus; actions: HostPolicyResponseAppliedAction[]; - policy: { - id: string; - version: string; - }; response: { configurations: { malware: HostPolicyResponseConfigurationStatus; diff --git a/x-pack/plugins/siem/scripts/endpoint/resolver_generator.ts b/x-pack/plugins/siem/scripts/endpoint/resolver_generator.ts index 77bf200eeb540..26c6e5ccc28a8 100644 --- a/x-pack/plugins/siem/scripts/endpoint/resolver_generator.ts +++ b/x-pack/plugins/siem/scripts/endpoint/resolver_generator.ts @@ -70,7 +70,7 @@ async function main() { metadataIndex: { alias: 'mi', describe: 'index to store host metadata in', - default: 'metrics-endpoint-default-1', + default: 'metrics-endpoint.metadata-default-1', type: 'string', }, policyIndex: { diff --git a/x-pack/plugins/siem/server/endpoint/alerts/index_pattern.ts b/x-pack/plugins/siem/server/endpoint/alerts/index_pattern.ts index 1cbdf96c5bcee..391aedecdd099 100644 --- a/x-pack/plugins/siem/server/endpoint/alerts/index_pattern.ts +++ b/x-pack/plugins/siem/server/endpoint/alerts/index_pattern.ts @@ -22,6 +22,7 @@ export interface IndexPatternRetriever { export class IngestIndexPatternRetriever implements IndexPatternRetriever { private static endpointPackageName = 'endpoint'; private static metadataDataset = 'metadata'; + private static policyDataset = 'policy'; private readonly log: Logger; constructor(private readonly service: ESIndexPatternService, loggerFactory: LoggerFactory) { this.log = loggerFactory.get('index-pattern-retriever'); @@ -76,7 +77,7 @@ export class IngestIndexPatternRetriever implements IndexPatternRetriever { } } - getPolicyResponseIndexPattern(ctx: RequestHandlerContext): Promise { - return Promise.resolve('metrics-endpoint.policy-default-1'); + async getPolicyResponseIndexPattern(ctx: RequestHandlerContext): Promise { + return this.getIndexPattern(ctx, IngestIndexPatternRetriever.policyDataset); } } diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts index ecdee09ce7edf..155513aefc609 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts @@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) { let nullableEventId = ''; - describe.skip('Endpoint alert API', () => { + describe('Endpoint alert API', () => { describe('when data is in elasticsearch', () => { before(async () => { await esArchiver.load('endpoint/alerts/api_feature'); diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts index df1cbcfe28e7b..ad9f4463c6419 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe.skip('Endpoint index pattern API', () => { + describe('Endpoint index pattern API', () => { it('should retrieve the index pattern for events', async () => { const { body } = await supertest.get('/api/endpoint/index_pattern/events').expect(200); expect(body.indexPattern).to.eql('events-endpoint-*'); @@ -17,7 +17,12 @@ export default function ({ getService }: FtrProviderContext) { it('should retrieve the index pattern for metadata', async () => { const { body } = await supertest.get('/api/endpoint/index_pattern/metadata').expect(200); - expect(body.indexPattern).to.eql('metrics-endpoint-*'); + expect(body.indexPattern).to.eql('metrics-endpoint.metadata-*'); + }); + + it('should retrieve the index pattern for policy', async () => { + const { body } = await supertest.get('/api/endpoint/index_pattern/policy').expect(200); + expect(body.indexPattern).to.eql('metrics-endpoint.policy-*'); }); it('should not retrieve the index pattern for an invalid key', async () => { diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index c01919f60a922..5c4bb52b8d9e2 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -14,7 +14,7 @@ const numberOfHostsInFixture = 3; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe.skip('test metadata api', () => { + describe('test metadata api', () => { describe('POST /api/endpoint/metadata when index is empty', () => { it('metadata api should return empty result when index is empty', async () => { await esArchiver.unload('endpoint/metadata/api_feature'); diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz index 3d4f0e11a7cc670366fa6098143b6306a1a1c72b..49082ed3bec8b0876ab3ea453581b858ce2b3d81 100644 GIT binary patch literal 855 zcmV-d1E~BTiwFppBGFy|17u-zVJ>QOZ*BnHmP>ErHW0_}{uGAK8G(->MfOy0nqm(@ z(PDuvilT?%%W-R4hAn$H4f5SP$|hMyavn$*dkA0|5;>z8@<04(u14eWq`h3{$q(a6 zYU1Ri8`#rg*Nt5913%7FH+y;h%Tm0bzDqyV=HvO_=YRZGe!nsAELJIh?u?eX-OS?_ z@+z&H`KqmWfD*L?DHqx;T99`>+h(_kU!(BIkL`S!w=FH#JSvStfvB}X<4^8PXqH#) zfzN$*nR37!5)_IVFsd2?3JcCgltiL-B1}%cdhPROJ8xE=`23$9bdAvLx@l;K{@cInS zl=Fb6VhYnKBDhnnrYP~}X+1qyGyAP*d872ld3!NF+w)rHtJ#xD?FaiEH<{X;`VtLX zFPd%7vm3d~;62|1g=M)|M)S|^P?|4`ix`rOp@6J?4Vbb7uc88luGYb8!`uA8t1Fs6 z4;L#%B?Vu#s1()@H=>)9x>ABKKbUg5ag9lV7kYr0&DP%q5Z2fi0Kz02GZxS?Vs|{bo@7!`Q_g}|hf+NUqxg$dG hpa`6Su;04ztD6WQ%D{rLX_z(6@!yl0h000txpKkyF literal 849 zcmV-X1FrlZiwFo(DwQOZ*BnHmQ8QtHV}sI{uPGL8G#%QsbNp`rYZIi z6fG9$q9}Ucr{mVP3|sbY8sxv1$|hMyaz2pmUIJK#M9yf2e20(bYBU~C+RJsC{4k!x zCQMGcfxTPox{<5$P#)*8o4q{$WhvfI-^HJ5{qg+o^FMyezu%a57OR*(cSg(9ZsuVN zX%*MaeAPls+3glB$UD<*v)hEPFE1lMw)17$wr;sDqf#>oBBeBGdva&uW@*(P_}nLz zT=JBBgzQ2B3Rr_DVSr5_>71xp_{qsvuYKBV=gq2fHI=IDT#g;YP>h;B}*3Pri>V2V}6vXXS5*Ix&rmM-*T=~8nRBtf!JwD97%XS3}qnRkoD z7e!XF`LrE>NSjicV-ymPOZVy4&{RExz1z&)jaa>x^~??nACG81y8FK#U5`drFXzHL z-{Zn;w*H=6FxtF;3p$#RD1(xLx>H%oNJfO(r6kJX(U$F_Trd^?e=cb6rS*j1HD@5= zazH5ws70t<#fR@o3d_%vq7K>S+9^PGrhIT@0`EzK5ZXE9EIEoN zNbwCi2%)U@2@NQ%3Mq`>!VFVBC|+^NKoeW_W=UcBHKYii|JAcaDTMWr4#FoELMSlS zbm$kkoHQpPTM7z2QL_43QV>_N^a6-@q=6{9WBCnd1a>`ydZY_DLJCeD2)qS!z#1a= bNFl(AQj>tC8akZ3k@n(0L$8A>b_@Uj^{t;- diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json index f9d5de0d0a94c..ffd64ab3bb939 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json @@ -2,7 +2,7 @@ "type": "index", "value": { "aliases": {}, - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "mappings": { "_meta": { "version": "1.5.0-dev" diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json index d3617dc236375..0f9f86b36dec7 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "3KVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -51,7 +51,7 @@ "type": "doc", "value": { "id": "3aVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -99,7 +99,7 @@ "type": "doc", "value": { "id": "3qVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -145,7 +145,7 @@ "type": "doc", "value": { "id": "36VN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -194,7 +194,7 @@ "type": "doc", "value": { "id": "4KVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -241,7 +241,7 @@ "type": "doc", "value": { "id": "4aVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -288,7 +288,7 @@ "type": "doc", "value": { "id": "4qVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579874769541, "agent": { @@ -336,7 +336,7 @@ "type": "doc", "value": { "id": "46VN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579874769541, "agent": { @@ -383,7 +383,7 @@ "type": "doc", "value": { "id": "5KVN2G8BYQH1gtPUuYk7", - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "source": { "@timestamp": 1579874769541, "agent": { diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json index f9d5de0d0a94c..ffd64ab3bb939 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json @@ -2,7 +2,7 @@ "type": "index", "value": { "aliases": {}, - "index": "metrics-endpoint-default-1", + "index": "metrics-endpoint.metadata-default-1", "mappings": { "_meta": { "version": "1.5.0-dev" From b83d145813e9e74bffb93705f384806fed2f3bd9 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 2 Jun 2020 16:52:19 -0400 Subject: [PATCH 55/69] [Endpoint][SIEM] Adjust types related to subplugin's redux (#67768) --- .../siem/common/endpoint_alerts/types.ts | 21 +--- x-pack/plugins/siem/public/app/types.ts | 52 ++++---- .../drag_drop_context_wrapper.tsx | 2 +- .../error_toast_dispatcher/index.test.tsx | 2 +- .../mock/endpoint/app_context_render.tsx | 8 +- .../siem/public/common/mock/global_state.ts | 19 +-- .../plugins/siem/public/common/mock/utils.ts | 22 ++-- .../siem/public/common/store/app/selectors.ts | 4 +- .../common/store/drag_and_drop/selectors.ts | 4 +- .../plugins/siem/public/common/store/index.ts | 38 +++++- .../public/common/store/inputs/selectors.ts | 2 +- .../siem/public/common/store/reducer.ts | 79 ++++++------ .../plugins/siem/public/common/store/store.ts | 15 ++- .../siem/public/common/store/test_utils.ts | 3 +- .../plugins/siem/public/common/store/types.ts | 112 ++++++++++++------ .../common/utils/clone_http_fetch_query.ts | 3 +- .../siem/public/endpoint_alerts/index.ts | 37 ++++-- .../endpoint_alerts/models/index_pattern.ts | 2 +- .../siem/public/endpoint_alerts/routes.tsx | 2 +- .../public/endpoint_alerts/store/action.ts | 4 +- .../store/alert_details.test.ts | 3 +- .../endpoint_alerts/store/alert_list.test.ts | 3 +- .../public/endpoint_alerts/store/index.ts | 20 ---- .../endpoint_alerts/store/middleware.ts | 3 +- .../public/endpoint_alerts/store/reducer.ts | 27 ++--- .../public/endpoint_alerts/store/selectors.ts | 2 +- .../view/alert_details.test.tsx | 2 +- .../view/details/metadata/file_accordion.tsx | 3 +- .../details/metadata/general_accordion.tsx | 3 +- .../view/details/metadata/hash_accordion.tsx | 3 +- .../view/details/metadata/host_accordion.tsx | 3 +- .../metadata/source_process_accordion.tsx | 3 +- .../source_process_token_accordion.tsx | 3 +- .../view/hooks/use_alerts_selector.ts | 5 +- .../endpoint_alerts/view/index.test.tsx | 2 +- .../siem/public/endpoint_hosts/index.ts | 35 ++++-- .../siem/public/endpoint_hosts/routes.tsx | 2 +- .../public/endpoint_hosts/store/index.test.ts | 3 +- .../siem/public/endpoint_hosts/store/index.ts | 22 ---- .../endpoint_hosts/store/middleware.test.ts | 3 +- .../public/endpoint_hosts/store/middleware.ts | 6 +- .../public/endpoint_hosts/store/reducer.ts | 32 +++-- .../siem/public/endpoint_hosts/view/hooks.ts | 2 +- .../siem/public/hosts/store/selectors.ts | 2 +- .../plugins/siem/public/management/index.ts | 42 +++++-- .../policy/store/policy_details/middleware.ts | 8 +- .../policy/store/policy_details/reducer.ts | 29 ++--- .../policy/store/policy_list/index.test.ts | 4 +- .../policy/store/policy_list/middleware.ts | 8 +- .../pages/policy/store/policy_list/reducer.ts | 26 ++-- .../plugins/siem/public/management/routes.tsx | 2 +- .../siem/public/management/store/index.ts | 8 -- .../public/management/store/middleware.ts | 29 ++--- .../siem/public/management/store/reducer.ts | 28 ++--- .../siem/public/management/store/types.ts | 26 ---- .../plugins/siem/public/management/types.ts | 7 ++ .../siem/public/network/store/selectors.ts | 2 +- .../public/timelines/store/timeline/model.ts | 2 +- .../timelines/store/timeline/selectors.ts | 2 +- 59 files changed, 438 insertions(+), 408 deletions(-) delete mode 100644 x-pack/plugins/siem/public/endpoint_alerts/store/index.ts delete mode 100644 x-pack/plugins/siem/public/endpoint_hosts/store/index.ts delete mode 100644 x-pack/plugins/siem/public/management/store/index.ts delete mode 100644 x-pack/plugins/siem/public/management/store/types.ts diff --git a/x-pack/plugins/siem/common/endpoint_alerts/types.ts b/x-pack/plugins/siem/common/endpoint_alerts/types.ts index 2df92b43ab52a..3fbde79414aa0 100644 --- a/x-pack/plugins/siem/common/endpoint_alerts/types.ts +++ b/x-pack/plugins/siem/common/endpoint_alerts/types.ts @@ -15,28 +15,9 @@ import { AlertEvent, KbnConfigSchemaInputTypeOf, AppLocation, + Immutable, } from '../endpoint/types'; -/** - * A deep readonly type that will make all children of a given object readonly recursively - */ -export type Immutable = T extends undefined | null | boolean | string | number - ? T - : unknown extends T - ? unknown - : T extends Array - ? ImmutableArray - : T extends Map - ? ImmutableMap - : T extends Set - ? ImmutableSet - : ImmutableObject; - -type ImmutableArray = ReadonlyArray>; -type ImmutableMap = ReadonlyMap, Immutable>; -type ImmutableSet = ReadonlySet>; -type ImmutableObject = { readonly [K in keyof T]: Immutable }; - /** * Values for the Alert APIs 'order' and 'direction' parameters. */ diff --git a/x-pack/plugins/siem/public/app/types.ts b/x-pack/plugins/siem/public/app/types.ts index 444e0066c3c7b..4b00dee3b7510 100644 --- a/x-pack/plugins/siem/public/app/types.ts +++ b/x-pack/plugins/siem/public/app/types.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer, AnyAction, Middleware, Dispatch } from 'redux'; +import { + Reducer, + AnyAction, + Middleware, + Dispatch, + PreloadedState, + StateFromReducersMapObject, + CombinedState, +} from 'redux'; + import { NavTab } from '../common/components/navigation/types'; -import { HostsState } from '../hosts/store'; -import { NetworkState } from '../network/store'; -import { TimelineState } from '../timelines/store/timeline/types'; -import { ImmutableReducer, State } from '../common/store'; +import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; -import { AlertListState } from '../../common/endpoint_alerts/types'; import { AppAction } from '../common/store/actions'; -import { HostState } from '../endpoint_hosts/types'; -import { ManagementState } from '../management/store/types'; export enum SiemPageName { overview = 'overview', @@ -38,7 +41,7 @@ export type SiemNavTabKey = export type SiemNavTab = Record; export interface SecuritySubPluginStore { - initialState: Record; + initialState: Record; reducer: Record>; middleware?: Array>>>; } @@ -54,6 +57,10 @@ type SecuritySubPluginKeyStore = | 'hostList' | 'alertList' | 'management'; + +/** + * Returned by the various 'SecuritySubPlugin' classes from the `start` method. + */ export interface SecuritySubPluginWithStore extends SecuritySubPlugin { store: SecuritySubPluginStore; @@ -61,22 +68,17 @@ export interface SecuritySubPluginWithStore; - hostList: Immutable; - management: ManagementState; - }; - reducer: { - hosts: Reducer; - network: Reducer; - timeline: Reducer; - alertList: ImmutableReducer; - hostList: ImmutableReducer; - management: ImmutableReducer; - }; + initialState: PreloadedState< + CombinedState< + StateFromReducersMapObject< + /** SubPluginsInitReducer, being an interface, will not work in `StateFromReducersMapObject`. + * Picking its keys does the trick. + **/ + Pick + > + > + >; + reducer: SubPluginsInitReducer; middlewares: Array>>>; }; } diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index c71cd8e60596c..c33677e41db0e 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -15,7 +15,7 @@ import { BrowserFields } from '../../containers/source'; import { dragAndDropModel, dragAndDropSelectors } from '../../store'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; -import { State } from '../../store/reducer'; +import { State } from '../../store/types'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { reArrangeProviders } from '../../../timelines/components/timeline/data_providers/helpers'; import { ACTIVE_TIMELINE_REDUX_ID } from '../top_n'; diff --git a/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx index 50b20099b17d0..39b17f7008e64 100644 --- a/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx @@ -12,7 +12,7 @@ import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '.. import { createStore } from '../../store/store'; import { ErrorToastDispatcher } from '.'; -import { State } from '../../store/reducer'; +import { State } from '../../store/types'; describe('Error Toast Dispatcher', () => { const state: State = mockGlobalState; diff --git a/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx index e62f36c2ec782..3c0189625ee29 100644 --- a/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx @@ -15,10 +15,10 @@ import { depsStartMock } from './dependencies_start_mock'; import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils'; import { apolloClientObservable } from '../test_providers'; import { createStore, State, substateMiddlewareFactory } from '../../store'; -import { hostMiddlewareFactory } from '../../../endpoint_hosts/store'; import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware'; import { AppRootProvider } from './app_root_provider'; -import { managementMiddlewareFactory } from '../../../management/store'; +import { managementMiddlewareFactory } from '../../../management/store/middleware'; +import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware'; import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -56,8 +56,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { const coreStart = coreMock.createStart({ basePath: '/mock' }); const depsStart = depsStartMock(); const middlewareSpy = createSpyMiddleware(); - const state: State = mockGlobalState; - const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, [ + const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, [ substateMiddlewareFactory( (globalState) => globalState.hostList, hostMiddlewareFactory(coreStart, depsStart) @@ -76,7 +75,6 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { ); const render: UiRender = (ui, options) => { - // @ts-ignore return reactRender(ui, { wrapper: AppWrapper as React.ComponentType, ...options, diff --git a/x-pack/plugins/siem/public/common/mock/global_state.ts b/x-pack/plugins/siem/public/common/mock/global_state.ts index c96f67a39dbfe..30dffa8dbf6bf 100644 --- a/x-pack/plugins/siem/public/common/mock/global_state.ts +++ b/x-pack/plugins/siem/public/common/mock/global_state.ts @@ -27,11 +27,10 @@ import { networkModel } from '../../network/store'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { initialAlertListState } from '../../endpoint_alerts/store/reducer'; import { initialHostListState } from '../../endpoint_hosts/store/reducer'; -import { getManagementInitialState } from '../../management/store'; - -const alertList = initialAlertListState(); -const hostList = initialHostListState(); -const management = getManagementInitialState(); +import { mockManagementState } from '../../management/store/reducer'; +import { AlertListState } from '../../../common/endpoint_alerts/types'; +import { HostState } from '../../endpoint_hosts/types'; +import { ManagementState } from '../../management/types'; export const mockGlobalState: State = { app: { @@ -233,7 +232,11 @@ export const mockGlobalState: State = { }, }, }, - alertList, - hostList, - management, + /** + * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, + * they are cast to mutable versions here. + */ + alertList: initialAlertListState as AlertListState, + hostList: initialHostListState as HostState, + management: mockManagementState as ManagementState, }; diff --git a/x-pack/plugins/siem/public/common/mock/utils.ts b/x-pack/plugins/siem/public/common/mock/utils.ts index 532637acab767..1ff5cb8e734ec 100644 --- a/x-pack/plugins/siem/public/common/mock/utils.ts +++ b/x-pack/plugins/siem/public/common/mock/utils.ts @@ -7,9 +7,13 @@ import { hostsReducer } from '../../hosts/store'; import { networkReducer } from '../../network/store'; import { timelineReducer } from '../../timelines/store/timeline/reducer'; -import { hostListReducer } from '../../endpoint_hosts/store'; -import { alertListReducer } from '../../endpoint_alerts/store'; -import { managementReducer } from '../../management/store'; +import { managementReducer } from '../../management/store/reducer'; +import { ManagementPluginReducer } from '../../management'; +import { SubPluginsInitReducer } from '../store'; +import { EndpointAlertsPluginReducer } from '../../endpoint_alerts'; +import { EndpointHostsPluginReducer } from '../../endpoint_hosts'; +import { alertListReducer } from '../../endpoint_alerts/store/reducer'; +import { hostListReducer } from '../../endpoint_hosts/store/reducer'; interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -18,11 +22,15 @@ interface Global extends NodeJS.Global { export const globalNode: Global = global; -export const SUB_PLUGINS_REDUCER = { +export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = { hosts: hostsReducer, network: networkReducer, timeline: timelineReducer, - hostList: hostListReducer, - alertList: alertListReducer, - management: managementReducer, + /** + * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, + * they are cast to mutable versions here. + */ + hostList: hostListReducer as EndpointHostsPluginReducer['hostList'], + alertList: alertListReducer as EndpointAlertsPluginReducer['alertList'], + management: managementReducer as ManagementPluginReducer['management'], }; diff --git a/x-pack/plugins/siem/public/common/store/app/selectors.ts b/x-pack/plugins/siem/public/common/store/app/selectors.ts index c37695c2ccbe6..d18cb73dbcfb9 100644 --- a/x-pack/plugins/siem/public/common/store/app/selectors.ts +++ b/x-pack/plugins/siem/public/common/store/app/selectors.ts @@ -7,11 +7,9 @@ import { keys } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { createSelector } from 'reselect'; - import { Note } from '../../lib/note'; -import { State } from '../reducer'; - import { ErrorModel, NotesById } from './model'; +import { State } from '../types'; const selectNotesById = (state: State): NotesById => state.app.notesById; diff --git a/x-pack/plugins/siem/public/common/store/drag_and_drop/selectors.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/selectors.ts index 99a8369783cdd..5d6534f96bc7a 100644 --- a/x-pack/plugins/siem/public/common/store/drag_and_drop/selectors.ts +++ b/x-pack/plugins/siem/public/common/store/drag_and_drop/selectors.ts @@ -5,10 +5,8 @@ */ import { createSelector } from 'reselect'; - -import { State } from '../reducer'; - import { IdToDataProvider } from './model'; +import { State } from '../types'; const selectDataProviders = (state: State): IdToDataProvider => state.dragAndDrop.dataProviders; diff --git a/x-pack/plugins/siem/public/common/store/index.ts b/x-pack/plugins/siem/public/common/store/index.ts index 96eb5e159909c..6227931b53268 100644 --- a/x-pack/plugins/siem/public/common/store/index.ts +++ b/x-pack/plugins/siem/public/common/store/index.ts @@ -8,18 +8,46 @@ export * from './model'; export * from './reducer'; export * from './selectors'; +import { Middleware, Dispatch } from 'redux'; import { createStore, getStore } from './store'; -import { SubstateMiddlewareFactory } from './types'; +import { ImmutableMiddleware, State } from './types'; +import { AppAction } from './actions'; +import { Immutable } from '../../../common/endpoint/types'; export { createStore, getStore }; -export const substateMiddlewareFactory: SubstateMiddlewareFactory = (selector, middleware) => { +/** + * Takes a selector and an `ImmutableMiddleware`. The + * middleware's version of `getState` will receive + * the result of the selector instead of the global state. + * + * This allows middleware to have knowledge of only a subsection of state. + * + * `selector` returns an `Immutable` version of the substate. + * `middleware` must be an `ImmutableMiddleware`. + * + * Returns a regular middleware, meant to be used with `applyMiddleware`. + */ +export const substateMiddlewareFactory = ( + selector: (state: State) => Substate | Immutable, + middleware: ImmutableMiddleware +): Middleware<{}, State, Dispatch>> => { return (api) => { const substateAPI = { ...api, - // Return just the substate instead of global state. - getState() { - return selector(api.getState()); + // Return the substate instead of global state. + getState(): Immutable { + /** + * The selector will receive the basic (mutable) version of state. This is because + * the top level state shape doesn't use `Immutable`. We cast the return value as `Immutable` + * so that the middleware won't be able to mutate state. + * + * Immutable enforces nothing structural about a type so casting + * a value as `Immutable` is safe as long as nothing else is going to mutate. + * Since the state came from the return value of a reducer, the reducer will (hopefully) + * not be mutating it. + */ + return selector(api.getState()) as Immutable; }, }; return middleware(substateAPI); diff --git a/x-pack/plugins/siem/public/common/store/inputs/selectors.ts b/x-pack/plugins/siem/public/common/store/inputs/selectors.ts index 95c463776e288..0eee5ebbfbf77 100644 --- a/x-pack/plugins/siem/public/common/store/inputs/selectors.ts +++ b/x-pack/plugins/siem/public/common/store/inputs/selectors.ts @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; -import { State } from '../reducer'; +import { State } from '../types'; import { InputsModel, InputsRange, GlobalQuery } from './model'; diff --git a/x-pack/plugins/siem/public/common/store/reducer.ts b/x-pack/plugins/siem/public/common/store/reducer.ts index e06543b8d7181..ba85fbef860d7 100644 --- a/x-pack/plugins/siem/public/common/store/reducer.ts +++ b/x-pack/plugins/siem/public/common/store/reducer.ts @@ -4,47 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers } from 'redux'; +import { combineReducers, PreloadedState, AnyAction, Reducer } from 'redux'; -import { appReducer, AppState, initialAppState } from './app'; -import { dragAndDropReducer, DragAndDropState, initialDragAndDropState } from './drag_and_drop'; -import { createInitialInputsState, initialInputsState, inputsReducer, InputsState } from './inputs'; +import { appReducer, initialAppState } from './app'; +import { dragAndDropReducer, initialDragAndDropState } from './drag_and_drop'; +import { createInitialInputsState, inputsReducer } from './inputs'; -import { HostsPluginState, HostsPluginReducer } from '../../hosts/store'; -import { NetworkPluginState, NetworkPluginReducer } from '../../network/store'; -import { TimelinePluginState, TimelinePluginReducer } from '../../timelines/store/timeline'; -import { - EndpointAlertsPluginState, - EndpointAlertsPluginReducer, -} from '../../endpoint_alerts/store'; -import { EndpointHostsPluginState, EndpointHostsPluginReducer } from '../../endpoint_hosts/store'; +import { HostsPluginReducer } from '../../hosts/store'; +import { NetworkPluginReducer } from '../../network/store'; +import { TimelinePluginReducer } from '../../timelines/store/timeline'; -import { ManagementPluginReducer, ManagementPluginState } from '../../management/store/types'; - -export interface State - extends HostsPluginState, - NetworkPluginState, - TimelinePluginState, - EndpointAlertsPluginState, - EndpointHostsPluginState, - ManagementPluginState { - app: AppState; - dragAndDrop: DragAndDropState; - inputs: InputsState; -} - -export const initialState: Pick = { - app: initialAppState, - dragAndDrop: initialDragAndDropState, - inputs: initialInputsState, -}; - -type SubPluginsInitState = HostsPluginState & - NetworkPluginState & - TimelinePluginState & - EndpointAlertsPluginState & - EndpointHostsPluginState & - ManagementPluginState; +import { SecuritySubPlugins } from '../../app/types'; +import { ManagementPluginReducer } from '../../management'; +import { EndpointAlertsPluginReducer } from '../../endpoint_alerts'; +import { EndpointHostsPluginReducer } from '../../endpoint_hosts'; +import { State } from './types'; +import { AppAction } from './actions'; export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & @@ -53,14 +28,28 @@ export type SubPluginsInitReducer = HostsPluginReducer & EndpointHostsPluginReducer & ManagementPluginReducer; -export const createInitialState = (pluginsInitState: SubPluginsInitState): State => ({ - ...initialState, - ...pluginsInitState, - inputs: createInitialInputsState(), -}); +/** + * Factory for the 'initialState' that is used to preload state into the Security App's redux store. + */ +export const createInitialState = ( + pluginsInitState: SecuritySubPlugins['store']['initialState'] +): PreloadedState => { + const preloadedState: PreloadedState = { + app: initialAppState, + dragAndDrop: initialDragAndDropState, + ...pluginsInitState, + inputs: createInitialInputsState(), + }; + return preloadedState; +}; -export const createReducer = (pluginsReducer: SubPluginsInitReducer) => - combineReducers({ +/** + * Factory for the Security app's redux reducer. + */ +export const createReducer: ( + pluginsReducer: SubPluginsInitReducer +) => Reducer = (pluginsReducer: SubPluginsInitReducer) => + combineReducers({ app: appReducer, dragAndDrop: dragAndDropReducer, inputs: inputsReducer, diff --git a/x-pack/plugins/siem/public/common/store/store.ts b/x-pack/plugins/siem/public/common/store/store.ts index 10ea61828ed36..276dcdcaedb85 100644 --- a/x-pack/plugins/siem/public/common/store/store.ts +++ b/x-pack/plugins/siem/public/common/store/store.ts @@ -12,6 +12,7 @@ import { Store, Middleware, Dispatch, + PreloadedState, } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; @@ -21,11 +22,12 @@ import { telemetryMiddleware } from '../lib/telemetry'; import { appSelectors } from './app'; import { timelineSelectors } from '../../timelines/store/timeline'; import { inputsSelectors } from './inputs'; -import { State, SubPluginsInitReducer, createReducer } from './reducer'; +import { SubPluginsInitReducer, createReducer } from './reducer'; import { createRootEpic } from './epic'; import { AppApolloClient } from '../lib/lib'; import { AppAction } from './actions'; import { Immutable } from '../../../common/endpoint/types'; +import { State } from './types'; type ComposeType = typeof compose; declare global { @@ -33,10 +35,17 @@ declare global { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ComposeType; } } +/** + * The Redux store type for the Security app. + */ +export type SecurityAppStore = Store; let store: Store | null = null; -export { SubPluginsInitReducer }; + +/** + * Factory for Security App's redux store. + */ export const createStore = ( - state: State, + state: PreloadedState, pluginsReducer: SubPluginsInitReducer, apolloClient: Observable, additionalMiddleware?: Array>>> diff --git a/x-pack/plugins/siem/public/common/store/test_utils.ts b/x-pack/plugins/siem/public/common/store/test_utils.ts index 511cdcb6f0672..89558c37909f7 100644 --- a/x-pack/plugins/siem/public/common/store/test_utils.ts +++ b/x-pack/plugins/siem/public/common/store/test_utils.ts @@ -5,9 +5,8 @@ */ import { Dispatch } from 'redux'; -import { State } from './reducer'; +import { State, ImmutableMiddlewareFactory } from './types'; import { AppAction } from './actions'; -import { ImmutableMiddlewareFactory } from './types'; /** * Utilities for testing Redux middleware diff --git a/x-pack/plugins/siem/public/common/store/types.ts b/x-pack/plugins/siem/public/common/store/types.ts index a4bfdeb30b438..b9942979beb1e 100644 --- a/x-pack/plugins/siem/public/common/store/types.ts +++ b/x-pack/plugins/siem/public/common/store/types.ts @@ -4,19 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - Dispatch, - Action as ReduxAction, - AnyAction as ReduxAnyAction, - Action, - Middleware, -} from 'redux'; +import { Dispatch, Action, Middleware, CombinedState } from 'redux'; import { CoreStart } from '../../../../../../src/core/public'; -import { Immutable } from '../../../common/endpoint_alerts/types'; -import { State } from './reducer'; import { StartPlugins } from '../../types'; import { AppAction } from './actions'; +import { Immutable } from '../../../common/endpoint/types'; +import { AppState } from './app/reducer'; +import { InputsState } from './inputs/reducer'; +import { HostsPluginState } from '../../hosts/store'; +import { DragAndDropState } from './drag_and_drop/reducer'; +import { TimelinePluginState } from '../../timelines/store/timeline'; +import { NetworkPluginState } from '../../network/store'; +import { EndpointAlertsPluginState } from '../../endpoint_alerts'; +import { EndpointHostsPluginState } from '../../endpoint_hosts'; +import { ManagementPluginState } from '../../management'; + +/** + * The redux `State` type for the Security App. + * We use `CombinedState` to wrap our shape because we create our reducer using `combineReducers`. + * `combineReducers` returns a type wrapped in `CombinedState`. + * `CombinedState` is required for redux to know what keys to make optional when preloaded state into a store. + */ +export type State = CombinedState< + HostsPluginState & + NetworkPluginState & + TimelinePluginState & + EndpointAlertsPluginState & + EndpointHostsPluginState & + ManagementPluginState & { + app: AppState; + dragAndDrop: DragAndDropState; + inputs: InputsState; + } +>; export type KueryFilterQueryKind = 'kuery' | 'lucene'; @@ -67,60 +88,73 @@ export type ImmutableMiddlewareFactory = ( * Middleware will be of the `ImmutableMiddleware` variety. Not able to directly * change actions or state. */ -export type ImmutableMultipleMiddlewareFactory = ( +export type SecuritySubPluginMiddlewareFactory = ( coreStart: CoreStart, depsStart: Pick -) => Array>; +) => Array>>>; /** - * Simple type for a redux selector. + * Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`. + * Use this type for all Reducers in order to help enforce our pattern of immutable state. */ -type Selector = (state: S) => R; +export type ImmutableReducer = ( + state: Immutable | undefined, + action: Immutable +) => S | Immutable; /** - * Takes a selector and an `ImmutableMiddleware`. The - * middleware's version of `getState` will receive - * the result of the selector instead of the global state. - * - * This allows middleware to have knowledge of only a subsection of state. - * - * `selector` returns an `Immutable` version of the substate. - * `middleware` must be an `ImmutableMiddleware`. - * - * Returns a regular middleware, meant to be used with `applyMiddleware`. + * A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation, + * but will enforce that `Immutable` versions of `state` and `action` are received. */ -export type SubstateMiddlewareFactory = ( - selector: Selector>, - middleware: ImmutableMiddleware -) => Middleware<{}, State, Dispatch>>; +export type ImmutableCombineReducers = >( + reducers: M +) => ImmutableReducer< + CombinedState>, + ActionFromImmutableReducersMapObject +>; /** - * Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`. - * Use this type for all Reducers in order to help enforce our pattern of immutable state. + * Helper type for `ImmutableCombineReducers`. Infers the combined state type from an immutable reducer map. */ -export type ImmutableReducer = ( - state: Immutable | undefined, - action: Immutable -) => State | Immutable; +type StateFromImmutableReducersMapObject = M extends ImmutableReducersMapObject + ? { [P in keyof M]: M[P] extends ImmutableReducer ? S : never } + : never; /** - * A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation, - * but will enforce that `Immutable` versions of `state` and `action` are received. + * Helper type for `ImmutableCombineReducers`. Infers the combined action type from an immutable reducer map. + */ +type ActionFromImmutableReducersMapObject = M extends ImmutableReducersMapObject + ? ActionFromImmutableReducer> + : never; + +/** + * Helper type for `ImmutableCombineReducers`. Infers the combined reducer type from an immutable reducer map. + */ +type ImmutableReducerFromImmutableReducersMapObject = M extends { + [P in keyof M]: infer R; +} + ? R extends ImmutableReducer + ? R + : never + : never; + +/** + * Helper type for `ImmutableCombineReducers`. Infers the action type for an immutable reducer. */ -export type ImmutableCombineReducers = ( - reducers: ImmutableReducersMapObject -) => ImmutableReducer; +type ActionFromImmutableReducer = R extends ImmutableReducer ? A : never; /** + * Helper type for `ImmutableCombineReducers`. * Like `redux`'s `ReducersMapObject` (which is used by `combineReducers`) but enforces that * the `state` and `action` received are `Immutable` versions. */ -type ImmutableReducersMapObject = { +type ImmutableReducersMapObject = { [K in keyof S]: ImmutableReducer; }; /** * A better type for createStructuredSelector. This doesn't support the options object. + * https://github.com/reduxjs/reselect/pull/454 */ export type CreateStructuredSelector = < SelectorMap extends { [key: string]: (...args: never[]) => unknown } diff --git a/x-pack/plugins/siem/public/common/utils/clone_http_fetch_query.ts b/x-pack/plugins/siem/public/common/utils/clone_http_fetch_query.ts index bfa433dc9f9ac..90b81df8bc21e 100644 --- a/x-pack/plugins/siem/public/common/utils/clone_http_fetch_query.ts +++ b/x-pack/plugins/siem/public/common/utils/clone_http_fetch_query.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable } from '../../../common/endpoint_alerts/types'; - import { HttpFetchQuery } from '../../../../../../src/core/public'; +import { Immutable } from '../../../common/endpoint/types'; export function cloneHttpFetchQuery(query: Immutable): HttpFetchQuery { const clone: HttpFetchQuery = {}; diff --git a/x-pack/plugins/siem/public/endpoint_alerts/index.ts b/x-pack/plugins/siem/public/endpoint_alerts/index.ts index 6380edbde6958..e7e45b95ceb91 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/index.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/index.ts @@ -4,15 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Reducer } from 'redux'; import { SecuritySubPluginWithStore } from '../app/types'; -import { getEndpointAlertsRoutes } from './routes'; -import { Immutable } from '../../common/endpoint/types'; -import { initialAlertListState, alertListReducer } from './store/reducer'; +import { endpointAlertsRoutes } from './routes'; +import { alertListReducer } from './store/reducer'; import { AlertListState } from '../../common/endpoint_alerts/types'; import { alertMiddlewareFactory } from './store/middleware'; import { substateMiddlewareFactory } from '../common/store'; import { CoreStart } from '../../../../../src/core/public'; import { StartPlugins } from '../types'; +import { AppAction } from '../common/store/actions'; + +/** + * Internally, our state is sometimes immutable, ignore that in our external + * interface. + */ +export interface EndpointAlertsPluginState { + alertList: AlertListState; +} + +/** + * Internally, we use `ImmutableReducer`, but we present a regular reducer + * externally for compatibility w/ regular redux. + */ +export interface EndpointAlertsPluginReducer { + alertList: Reducer; +} export class EndpointAlerts { public setup() {} @@ -20,20 +37,24 @@ export class EndpointAlerts { public start( core: CoreStart, plugins: StartPlugins - ): SecuritySubPluginWithStore<'alertList', Immutable> { + ): SecuritySubPluginWithStore<'alertList', AlertListState> { const { data, ingestManager } = plugins; const middleware = [ - substateMiddlewareFactory( + substateMiddlewareFactory( (globalState) => globalState.alertList, alertMiddlewareFactory(core, { data, ingestManager }) ), ]; return { - routes: getEndpointAlertsRoutes(), + routes: endpointAlertsRoutes(), store: { - initialState: { alertList: initialAlertListState() }, - reducer: { alertList: alertListReducer }, + initialState: { alertList: undefined }, + /** + * Cast the ImmutableReducer to a regular reducer for compatibility with + * the subplugin architecture (which expects plain redux reducers.) + */ + reducer: { alertList: alertListReducer } as EndpointAlertsPluginReducer, middleware, }, }; diff --git a/x-pack/plugins/siem/public/endpoint_alerts/models/index_pattern.ts b/x-pack/plugins/siem/public/endpoint_alerts/models/index_pattern.ts index 8daaa3fef7a29..3eb347c6cada8 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/models/index_pattern.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/models/index_pattern.ts @@ -6,7 +6,7 @@ import { all } from 'deepmerge'; import { IIndexPattern } from 'src/plugins/data/public'; -import { Immutable } from '../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../common/endpoint/types'; /** * Model for the `IIndexPattern` interface exported by the `data` plugin. diff --git a/x-pack/plugins/siem/public/endpoint_alerts/routes.tsx b/x-pack/plugins/siem/public/endpoint_alerts/routes.tsx index 60df7f5d47129..d62ef20c384dc 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/routes.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/routes.tsx @@ -9,7 +9,7 @@ import { Route } from 'react-router-dom'; import { AlertIndex } from './view'; -export const getEndpointAlertsRoutes = () => [ +export const endpointAlertsRoutes = () => [ , diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/action.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/action.ts index ae103edaa5a2b..3330cef1816a7 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/action.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/store/action.ts @@ -5,8 +5,8 @@ */ import { IIndexPattern } from 'src/plugins/data/public'; -// import { Immutable } from '../../../common/types'; -import { AlertDetails, AlertListData, Immutable } from '../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../common/endpoint/types'; +import { AlertDetails, AlertListData } from '../../../common/endpoint_alerts/types'; interface ServerReturnedAlertsData { readonly type: 'serverReturnedAlertsData'; diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/alert_details.test.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/alert_details.test.ts index b634e6455d971..8e20ad089b9c2 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/alert_details.test.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/store/alert_details.test.ts @@ -8,7 +8,7 @@ import { Store, createStore, applyMiddleware } from 'redux'; import { createBrowserHistory, History } from 'history'; import { coreMock } from '../../../../../../src/core/public/mocks'; -import { AlertListState, Immutable } from '../../../common/endpoint_alerts/types'; +import { AlertListState } from '../../../common/endpoint_alerts/types'; import { depsStartMock, DepsStartMock } from '../../common/mock/endpoint'; import { alertListReducer } from './reducer'; @@ -16,6 +16,7 @@ import { alertListReducer } from './reducer'; import { alertMiddlewareFactory } from './middleware'; import { mockAlertResultList } from './mock_alert_result_list'; +import { Immutable } from '../../../common/endpoint/types'; describe('alert details tests', () => { let store: Store; diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/alert_list.test.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/alert_list.test.ts index 8f82c2522ea69..a21e449552960 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/alert_list.test.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/store/alert_list.test.ts @@ -7,12 +7,13 @@ import { Store, createStore, applyMiddleware } from 'redux'; import { History, createBrowserHistory } from 'history'; import { alertListReducer } from './reducer'; -import { AlertListState, AlertResultList, Immutable } from '../../../common/endpoint_alerts/types'; +import { AlertListState, AlertResultList } from '../../../common/endpoint_alerts/types'; import { alertMiddlewareFactory } from './middleware'; import { coreMock } from 'src/core/public/mocks'; import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint'; import { isOnAlertPage } from './selectors'; import { mockAlertResultList } from './mock_alert_result_list'; +import { Immutable } from '../../../common/endpoint/types'; describe('alert list tests', () => { let store: Store; diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/index.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/index.ts deleted file mode 100644 index dd97d60c532b0..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AlertListState, Immutable } from '../../../common/endpoint_alerts/types'; -import { ImmutableReducer } from '../../common/store'; -import { AppAction } from '../../common/store/actions'; - -export { alertListReducer } from './reducer'; -export { AlertAction } from './action'; - -export interface EndpointAlertsPluginState { - alertList: Immutable; -} - -export interface EndpointAlertsPluginReducer { - alertList: ImmutableReducer; -} diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/middleware.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/middleware.ts index b8e8d36801d48..dd84b4fcff5bd 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/middleware.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/store/middleware.ts @@ -20,9 +20,8 @@ import { uiQueryParams, isAlertPageTabChange, } from './selectors'; -import { Immutable } from '../../../common/endpoint/types'; -export const alertMiddlewareFactory: ImmutableMiddlewareFactory> = ( +export const alertMiddlewareFactory: ImmutableMiddlewareFactory = ( coreStart, depsStart ) => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/reducer.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/reducer.ts index 3e79ad4d1c614..22fc5025656d7 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/reducer.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/store/reducer.ts @@ -4,26 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable, AlertListState } from '../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../common/endpoint/types'; +import { AlertListState } from '../../../common/endpoint_alerts/types'; import { ImmutableReducer } from '../../common/store'; import { AppAction } from '../../common/store/actions'; -export const initialAlertListState = (): Immutable => { - return { - alerts: [], - alertDetails: undefined, - pageSize: 10, - pageIndex: 0, - total: 0, - location: undefined, - searchBar: { - patterns: [], - }, - }; +export const initialAlertListState: Immutable = { + alerts: [], + alertDetails: undefined, + pageSize: 10, + pageIndex: 0, + total: 0, + location: undefined, + searchBar: { + patterns: [], + }, }; export const alertListReducer: ImmutableReducer = ( - state = initialAlertListState(), + state = initialAlertListState, action ) => { if (action.type === 'serverReturnedAlertsData') { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/store/selectors.ts b/x-pack/plugins/siem/public/endpoint_alerts/store/selectors.ts index bec524f948d76..ab0e4165a2577 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/store/selectors.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/store/selectors.ts @@ -12,10 +12,10 @@ import { } from 'reselect'; import { encode, decode } from 'rison-node'; +import { Immutable } from '../../../common/endpoint/types'; import { Query, TimeRange, Filter } from '../../../../../../src/plugins/data/public'; import { - Immutable, AlertingIndexGetQueryInput, AlertListState, AlertingIndexUIQueryParams, diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/alert_details.test.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/alert_details.test.tsx index a4fe4811fa602..de939ad4f54c6 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/alert_details.test.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/alert_details.test.tsx @@ -11,7 +11,7 @@ import { Store } from 'redux'; import { mockAlertDetailsResult } from '../store/mock_alert_result_list'; import { alertPageTestRender } from './test_helpers/render_alert_page'; import { AppAction } from '../../common/store/actions'; -import { State } from '../../common/store/reducer'; +import { State } from '../../common/store/types'; describe('when the alert details flyout is open', () => { let render: () => reactTestingLibrary.RenderResult; diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/file_accordion.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/file_accordion.tsx index 1009bec0cec0e..e319cbe52a69c 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/file_accordion.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/file_accordion.tsx @@ -6,7 +6,8 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; -import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { AlertData } from '../../../../../common/endpoint_alerts/types'; import { FormattedDate } from '../../formatted_date'; export const FileAccordion = memo(({ alertData }: { alertData: Immutable }) => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/general_accordion.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/general_accordion.tsx index fc0d38188fd27..76ee46cb5a107 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/general_accordion.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/general_accordion.tsx @@ -6,8 +6,9 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; -import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types'; +import { AlertData } from '../../../../../common/endpoint_alerts/types'; import { FormattedDate } from '../../formatted_date'; +import { Immutable } from '../../../../../common/endpoint/types'; export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable }) => { const columns = useMemo(() => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/hash_accordion.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/hash_accordion.tsx index ae62bd80b73bc..3077a98905dff 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/hash_accordion.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/hash_accordion.tsx @@ -6,7 +6,8 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; -import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { AlertData } from '../../../../../common/endpoint_alerts/types'; export const HashAccordion = memo(({ alertData }: { alertData: Immutable }) => { const columns = useMemo(() => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/host_accordion.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/host_accordion.tsx index 70723efd97b8c..bea49cbba06ba 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/host_accordion.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/host_accordion.tsx @@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiDescriptionList, EuiHealth } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Immutable, AlertDetails } from '../../../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { AlertDetails } from '../../../../../common/endpoint_alerts/types'; export const HostAccordion = memo(({ alertData }: { alertData: Immutable }) => { const columns = useMemo(() => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_accordion.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_accordion.tsx index 607327a49de1c..b2d0e369bc453 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_accordion.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_accordion.tsx @@ -6,7 +6,8 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; -import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { AlertData } from '../../../../../common/endpoint_alerts/types'; export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutable }) => { const columns = useMemo(() => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_token_accordion.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_token_accordion.tsx index 9be494d92a88d..e559daa0a6350 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_token_accordion.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/details/metadata/source_process_token_accordion.tsx @@ -6,7 +6,8 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; -import { Immutable, AlertData } from '../../../../../common/endpoint_alerts/types'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { AlertData } from '../../../../../common/endpoint_alerts/types'; export const SourceProcessTokenAccordion = memo( ({ alertData }: { alertData: Immutable }) => { diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/hooks/use_alerts_selector.ts b/x-pack/plugins/siem/public/endpoint_alerts/view/hooks/use_alerts_selector.ts index 726f6a453cb5d..95c347893b99a 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/hooks/use_alerts_selector.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/hooks/use_alerts_selector.ts @@ -5,8 +5,9 @@ */ import { useSelector } from 'react-redux'; -import { Immutable, AlertListState } from '../../../../common/endpoint_alerts/types'; -import { State } from '../../../common/store/reducer'; +import { Immutable } from '../../../../common/endpoint/types'; +import { AlertListState } from '../../../../common/endpoint_alerts/types'; +import { State } from '../../../common/store/types'; export function useAlertListSelector( selector: ( diff --git a/x-pack/plugins/siem/public/endpoint_alerts/view/index.test.tsx b/x-pack/plugins/siem/public/endpoint_alerts/view/index.test.tsx index 4967d7661a085..3d056ca5c188f 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/view/index.test.tsx +++ b/x-pack/plugins/siem/public/endpoint_alerts/view/index.test.tsx @@ -12,7 +12,7 @@ import { Store } from 'redux'; import { mockAlertResultList } from '../store/mock_alert_result_list'; import { alertPageTestRender } from './test_helpers/render_alert_page'; import { DepsStartMock } from '../../common/mock/endpoint'; -import { State } from '../../common/store/reducer'; +import { State } from '../../common/store/types'; import { AppAction } from '../../common/store/actions'; describe('when on the alerting page', () => { diff --git a/x-pack/plugins/siem/public/endpoint_hosts/index.ts b/x-pack/plugins/siem/public/endpoint_hosts/index.ts index 1c2649ec5cf91..bd1c5f96f8cda 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/index.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/index.ts @@ -4,15 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Reducer } from 'redux'; import { SecuritySubPluginWithStore } from '../app/types'; -import { getEndpointHostsRoutes } from './routes'; -import { initialHostListState, hostListReducer } from './store/reducer'; -import { Immutable } from '../../common/endpoint/types'; +import { endpointHostsRoutes } from './routes'; +import { hostListReducer } from './store/reducer'; import { HostState } from './types'; import { hostMiddlewareFactory } from './store/middleware'; import { CoreStart } from '../../../../../src/core/public'; import { StartPlugins } from '../types'; import { substateMiddlewareFactory } from '../common/store'; +import { AppAction } from '../common/store/actions'; + +/** + * Internally, our state is sometimes immutable, ignore that in our external + * interface. + */ +export interface EndpointHostsPluginState { + hostList: HostState; +} + +/** + * Internally, we use `ImmutableReducer`, but we present a regular reducer + * externally for compatibility w/ regular redux. + */ +export interface EndpointHostsPluginReducer { + hostList: Reducer; +} export class EndpointHosts { public setup() {} @@ -20,7 +37,7 @@ export class EndpointHosts { public start( core: CoreStart, plugins: StartPlugins - ): SecuritySubPluginWithStore<'hostList', Immutable> { + ): SecuritySubPluginWithStore<'hostList', HostState> { const { data, ingestManager } = plugins; const middleware = [ substateMiddlewareFactory( @@ -29,10 +46,14 @@ export class EndpointHosts { ), ]; return { - routes: getEndpointHostsRoutes(), + routes: endpointHostsRoutes(), store: { - initialState: { hostList: initialHostListState() }, - reducer: { hostList: hostListReducer }, + initialState: { hostList: undefined }, + /** + * Cast the ImmutableReducer to a regular reducer for compatibility with + * the subplugin architecture (which expects plain redux reducers.) + */ + reducer: { hostList: hostListReducer } as EndpointHostsPluginReducer, middleware, }, }; diff --git a/x-pack/plugins/siem/public/endpoint_hosts/routes.tsx b/x-pack/plugins/siem/public/endpoint_hosts/routes.tsx index b7e549dc4e5e8..4ff9ecfaeab0e 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/routes.tsx +++ b/x-pack/plugins/siem/public/endpoint_hosts/routes.tsx @@ -9,7 +9,7 @@ import { Route } from 'react-router-dom'; import { HostList } from './view'; -export const getEndpointHostsRoutes = () => [ +export const endpointHostsRoutes = () => [ , diff --git a/x-pack/plugins/siem/public/endpoint_hosts/store/index.test.ts b/x-pack/plugins/siem/public/endpoint_hosts/store/index.test.ts index 8518c37fe3f5d..71452993abf07 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/store/index.test.ts @@ -5,10 +5,11 @@ */ import { createStore, Dispatch, Store } from 'redux'; -import { HostAction, hostListReducer } from './index'; import { HostState } from '../types'; import { listData } from './selectors'; import { mockHostResultList } from './mock_host_result_list'; +import { HostAction } from './action'; +import { hostListReducer } from './reducer'; describe('HostList store concerns', () => { let store: Store; diff --git a/x-pack/plugins/siem/public/endpoint_hosts/store/index.ts b/x-pack/plugins/siem/public/endpoint_hosts/store/index.ts deleted file mode 100644 index eafea5b9c7404..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_hosts/store/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { HostState } from '../types'; -import { ImmutableReducer } from '../../common/store'; -import { AppAction } from '../../common/store/actions'; -import { Immutable } from '../../../common/endpoint/types'; - -export { hostListReducer } from './reducer'; -export { HostAction } from './action'; -export { hostMiddlewareFactory } from './middleware'; - -export interface EndpointHostsPluginState { - hostList: Immutable; -} - -export interface EndpointHostsPluginReducer { - hostList: ImmutableReducer; -} diff --git a/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.test.ts index 6c9e3dd41907f..0959a3438aad9 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.test.ts @@ -7,7 +7,6 @@ import { CoreStart, HttpSetup } from 'kibana/public'; import { applyMiddleware, createStore, Store } from 'redux'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; -import { hostListReducer, hostMiddlewareFactory } from './index'; import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint'; @@ -17,6 +16,8 @@ import { AppAction } from '../../common/store/actions'; import { mockHostResultList } from './mock_host_result_list'; import { listData } from './selectors'; import { HostState } from '../types'; +import { hostListReducer } from './reducer'; +import { hostMiddlewareFactory } from './middleware'; describe('host list middleware', () => { let fakeCoreStart: jest.Mocked; diff --git a/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.ts b/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.ts index b9d8475601023..dd9ab19a702ea 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/store/middleware.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostResultList, Immutable } from '../../../common/endpoint/types'; +import { HostResultList } from '../../../common/endpoint/types'; import { ImmutableMiddlewareFactory } from '../../common/store'; import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; import { HostState } from '../types'; -export const hostMiddlewareFactory: ImmutableMiddlewareFactory> = ( - coreStart -) => { +export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (coreStart) => { return ({ getState, dispatch }) => (next) => async (action) => { next(action); const state = getState(); diff --git a/x-pack/plugins/siem/public/endpoint_hosts/store/reducer.ts b/x-pack/plugins/siem/public/endpoint_hosts/store/reducer.ts index 98f4a457a4598..c0d5e6931db2b 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/store/reducer.ts @@ -10,26 +10,24 @@ import { AppAction } from '../../common/store/actions'; import { ImmutableReducer } from '../../common/store'; import { Immutable } from '../../../common/endpoint/types'; -export const initialHostListState = (): HostState => { - return { - hosts: [], - pageSize: 10, - pageIndex: 0, - total: 0, - loading: false, - error: undefined, - details: undefined, - detailsLoading: false, - detailsError: undefined, - policyResponse: undefined, - policyResponseLoading: false, - policyResponseError: undefined, - location: undefined, - }; +export const initialHostListState: Immutable = { + hosts: [], + pageSize: 10, + pageIndex: 0, + total: 0, + loading: false, + error: undefined, + details: undefined, + detailsLoading: false, + detailsError: undefined, + policyResponse: undefined, + policyResponseLoading: false, + policyResponseError: undefined, + location: undefined, }; export const hostListReducer: ImmutableReducer = ( - state = initialHostListState(), + state = initialHostListState, action ) => { if (action.type === 'serverReturnedHostList') { diff --git a/x-pack/plugins/siem/public/endpoint_hosts/view/hooks.ts b/x-pack/plugins/siem/public/endpoint_hosts/view/hooks.ts index 6d552831c2fec..78fd679f818b6 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/view/hooks.ts @@ -8,7 +8,7 @@ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; import { HostState } from '../types'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { State } from '../../common/store/reducer'; +import { State } from '../../common/store/types'; export function useHostSelector(selector: (state: HostState) => TSelected) { return useSelector(function (state: State) { diff --git a/x-pack/plugins/siem/public/hosts/store/selectors.ts b/x-pack/plugins/siem/public/hosts/store/selectors.ts index aee93441e9409..61fa44a5f41d1 100644 --- a/x-pack/plugins/siem/public/hosts/store/selectors.ts +++ b/x-pack/plugins/siem/public/hosts/store/selectors.ts @@ -7,7 +7,7 @@ import { get } from 'lodash/fp'; import { createSelector } from 'reselect'; -import { State } from '../../common/store/reducer'; +import { State } from '../../common/store/types'; import { GenericHostsModel, HostsType, HostsTableType } from './model'; diff --git a/x-pack/plugins/siem/public/management/index.ts b/x-pack/plugins/siem/public/management/index.ts index 86522df110dfb..d6a723e5340bf 100644 --- a/x-pack/plugins/siem/public/management/index.ts +++ b/x-pack/plugins/siem/public/management/index.ts @@ -5,33 +5,53 @@ */ import { CoreStart } from 'kibana/public'; -import { managementReducer, getManagementInitialState, managementMiddlewareFactory } from './store'; -import { getManagementRoutes } from './routes'; +import { Reducer, CombinedState } from 'redux'; +import { managementRoutes } from './routes'; import { StartPlugins } from '../types'; -import { MANAGEMENT_STORE_GLOBAL_NAMESPACE } from './common/constants'; import { SecuritySubPluginWithStore } from '../app/types'; -import { Immutable } from '../../common/endpoint/types'; -import { ManagementStoreGlobalNamespace } from './types'; -import { ManagementState } from './store/types'; +import { managementReducer } from './store/reducer'; +import { AppAction } from '../common/store/actions'; +import { managementMiddlewareFactory } from './store/middleware'; +import { ManagementState } from './types'; export { getManagementUrl } from './common/routing'; +/** + * Internally, our state is sometimes immutable, ignore that in our external + * interface. + */ +export interface ManagementPluginState { + management: ManagementState; +} + +/** + * Internally, we use `ImmutableReducer`, but we present a regular reducer + * externally for compatibility w/ regular redux. + */ +export interface ManagementPluginReducer { + management: Reducer, AppAction>; +} + export class Management { public setup() {} public start( core: CoreStart, plugins: StartPlugins - ): SecuritySubPluginWithStore> { + ): SecuritySubPluginWithStore<'management', ManagementState> { return { - routes: getManagementRoutes(), + routes: managementRoutes(), store: { initialState: { - [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: getManagementInitialState(), + management: undefined, }, + /** + * Cast the ImmutableReducer to a regular reducer for compatibility with + * the subplugin architecture (which expects plain redux reducers.) + */ reducer: { - [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: managementReducer, - }, + management: managementReducer, + } as ManagementPluginReducer, middleware: managementMiddlewareFactory(core, plugins), }, }; diff --git a/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts index 97cdcac0fcae9..ec0c526482b45 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts @@ -16,13 +16,13 @@ import { sendGetFleetAgentStatusForConfig, sendPutDatasource, } from '../policy_list/services/ingest'; -import { NewPolicyData, PolicyData, Immutable } from '../../../../../../common/endpoint/types'; +import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types'; import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; -export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory> = (coreStart) => { +export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( + coreStart +) => { const http = coreStart.http; return ({ getState, dispatch }) => (next) => async (action) => { diff --git a/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/reducer.ts index 95daad7ae5977..75e7808ea30b1 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/reducer.ts @@ -4,24 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; -import { PolicyDetailsState } from '../../types'; import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../../common/endpoint/types'; import { ImmutableReducer } from '../../../../../common/store'; import { AppAction } from '../../../../../common/store/actions'; +import { PolicyDetailsState } from '../../types'; -export const initialPolicyDetailsState = (): PolicyDetailsState => { - return { - policyItem: undefined, - isLoading: false, - agentStatusSummary: { - error: 0, - events: 0, - offline: 0, - online: 0, - total: 0, - }, - }; -}; +/** + * Return a fresh copy of initial state, since we mutate state in the reducer. + */ +export const initialPolicyDetailsState: () => Immutable = () => ({ + policyItem: undefined, + isLoading: false, + agentStatusSummary: { + error: 0, + events: 0, + offline: 0, + online: 0, + total: 0, + }, +}); export const policyDetailsReducer: ImmutableReducer = ( state = initialPolicyDetailsState(), diff --git a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts index c796edff8aabc..a312134bbcd22 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts @@ -10,7 +10,7 @@ import { Store, applyMiddleware, createStore } from 'redux'; import { coreMock } from '../../../../../../../../../src/core/public/mocks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common'; -import { policyListReducer, initialPolicyListState } from './reducer'; +import { policyListReducer } from './reducer'; import { policyListMiddlewareFactory } from './middleware'; import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors'; @@ -39,7 +39,7 @@ describe('policy list store concerns', () => { store = createStore( policyListReducer, - initialPolicyListState(), + undefined, applyMiddleware(policyListMiddlewareFactory(fakeCoreStart, depsStart), actionSpyMiddleware) ); }); diff --git a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts index 7259c0fd4d21c..66962c378537f 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts @@ -8,10 +8,9 @@ import { GetPolicyListResponse, PolicyListState } from '../../types'; import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; -import { Immutable } from '../../../../../../common/endpoint/types'; import { initialPolicyListState } from './reducer'; -export const policyListMiddlewareFactory: ImmutableMiddlewareFactory> = ( +export const policyListMiddlewareFactory: ImmutableMiddlewareFactory = ( coreStart ) => { const http = coreStart.http; @@ -20,7 +19,6 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory { - return { - policyItems: [], - isLoading: false, - apiError: undefined, - pageIndex: 0, - pageSize: 10, - total: 0, - location: undefined, - }; -}; +/** + * Return the initial state. + * In case `state` was mutated, we return a fresh initial state object. + */ +export const initialPolicyListState: () => Immutable = () => ({ + policyItems: [], + isLoading: false, + apiError: undefined, + pageIndex: 0, + pageSize: 10, + total: 0, + location: undefined, +}); export const policyListReducer: ImmutableReducer = ( state = initialPolicyListState(), diff --git a/x-pack/plugins/siem/public/management/routes.tsx b/x-pack/plugins/siem/public/management/routes.tsx index fbcea37c76962..12727ea97458e 100644 --- a/x-pack/plugins/siem/public/management/routes.tsx +++ b/x-pack/plugins/siem/public/management/routes.tsx @@ -12,7 +12,7 @@ import { MANAGEMENT_ROUTING_ROOT_PATH } from './common/constants'; /** * Returns the React Router Routes for the management area */ -export const getManagementRoutes = () => [ +export const managementRoutes = () => [ // Mounts the Management interface on `/management` , ]; diff --git a/x-pack/plugins/siem/public/management/store/index.ts b/x-pack/plugins/siem/public/management/store/index.ts deleted file mode 100644 index 50049f9828082..0000000000000 --- a/x-pack/plugins/siem/public/management/store/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { managementReducer, getManagementInitialState } from './reducer'; -export { managementMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/management/store/middleware.ts b/x-pack/plugins/siem/public/management/store/middleware.ts index f73736e04a5b7..c8eb27e35f9dd 100644 --- a/x-pack/plugins/siem/public/management/store/middleware.ts +++ b/x-pack/plugins/siem/public/management/store/middleware.ts @@ -4,30 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ImmutableMultipleMiddlewareFactory, substateMiddlewareFactory } from '../../common/store'; +import { + substateMiddlewareFactory, + SecuritySubPluginMiddlewareFactory, + State, +} from '../../common/store'; import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; -import { - MANAGEMENT_STORE_GLOBAL_NAMESPACE, - MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, - MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, -} from '../common/constants'; -// @ts-ignore -export const managementMiddlewareFactory: ImmutableMultipleMiddlewareFactory = ( +export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( coreStart, depsStart ) => { + const listSelector = (state: State) => state.management.policyList; + const detailSelector = (state: State) => state.management.policyDetails; + return [ - substateMiddlewareFactory( - (globalState) => - globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE], - policyListMiddlewareFactory(coreStart, depsStart) - ), - substateMiddlewareFactory( - (globalState) => - globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE], - policyDetailsMiddlewareFactory(coreStart, depsStart) - ), + substateMiddlewareFactory(listSelector, policyListMiddlewareFactory(coreStart, depsStart)), + substateMiddlewareFactory(detailSelector, policyDetailsMiddlewareFactory(coreStart, depsStart)), ]; }; diff --git a/x-pack/plugins/siem/public/management/store/reducer.ts b/x-pack/plugins/siem/public/management/store/reducer.ts index ba7927684ad3d..64b2ab5c05f96 100644 --- a/x-pack/plugins/siem/public/management/store/reducer.ts +++ b/x-pack/plugins/siem/public/management/store/reducer.ts @@ -4,42 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers as reduxCombineReducers } from 'redux'; +import { combineReducers } from 'redux'; import { - initialPolicyDetailsState, policyDetailsReducer, + initialPolicyDetailsState, } from '../pages/policy/store/policy_details/reducer'; import { - initialPolicyListState, policyListReducer, + initialPolicyListState, } from '../pages/policy/store/policy_list/reducer'; import { MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, } from '../common/constants'; import { ImmutableCombineReducers } from '../../common/store'; -import { AppAction } from '../../common/store/actions'; -import { ManagementState } from './types'; +import { Immutable } from '../../../common/endpoint/types'; +import { ManagementState } from '../types'; -// Change the type of `combinerReducers` locally -const combineReducers: ImmutableCombineReducers = reduxCombineReducers; +const immutableCombineReducers: ImmutableCombineReducers = combineReducers; -/** - * Returns the initial state of the store for the SIEM Management section - */ -export const getManagementInitialState = (): ManagementState => { - return { - [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), - [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), - }; +export const mockManagementState: Immutable = { + policyList: initialPolicyListState(), + policyDetails: initialPolicyDetailsState(), }; /** * Redux store reducer for the SIEM Management section */ -export const managementReducer = combineReducers({ - // @ts-ignore +export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, - // @ts-ignore [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, }); diff --git a/x-pack/plugins/siem/public/management/store/types.ts b/x-pack/plugins/siem/public/management/store/types.ts deleted file mode 100644 index 884724982fa8f..0000000000000 --- a/x-pack/plugins/siem/public/management/store/types.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Immutable } from '../../../common/endpoint/types'; -import { PolicyDetailsState, PolicyListState } from '../pages/policy/types'; -import { ImmutableReducer } from '../../common/store'; -import { AppAction } from '../../common/store/actions'; - -/** - * Redux store state for the Management section - */ -export interface ManagementState { - policyDetails: Immutable; - policyList: Immutable; -} - -export interface ManagementPluginState { - management: ManagementState; -} - -export interface ManagementPluginReducer { - management: ImmutableReducer; -} diff --git a/x-pack/plugins/siem/public/management/types.ts b/x-pack/plugins/siem/public/management/types.ts index 5ee16bcd434e3..eeeafb4cbe150 100644 --- a/x-pack/plugins/siem/public/management/types.ts +++ b/x-pack/plugins/siem/public/management/types.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CombinedState } from 'redux'; import { SiemPageName } from '../app/types'; +import { PolicyListState, PolicyDetailsState } from './pages/policy/types'; /** * The type for the management store global namespace. Used mostly internally to reference @@ -12,6 +14,11 @@ import { SiemPageName } from '../app/types'; */ export type ManagementStoreGlobalNamespace = 'management'; +export type ManagementState = CombinedState<{ + policyList: PolicyListState; + policyDetails: PolicyDetailsState; +}>; + /** * The management list of sub-tabs. Changes to these will impact the Router routes. */ diff --git a/x-pack/plugins/siem/public/network/store/selectors.ts b/x-pack/plugins/siem/public/network/store/selectors.ts index eac373cd63ebd..cef8b139402ef 100644 --- a/x-pack/plugins/siem/public/network/store/selectors.ts +++ b/x-pack/plugins/siem/public/network/store/selectors.ts @@ -8,7 +8,7 @@ import { createSelector } from 'reselect'; import { get } from 'lodash/fp'; import { FlowTargetSourceDest } from '../../graphql/types'; -import { State } from '../../common/store/reducer'; +import { State } from '../../common/store/types'; import { initialNetworkState } from './reducer'; import { IpDetailsTableType, diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/model.ts b/x-pack/plugins/siem/public/timelines/store/timeline/model.ts index df5e2a99e0248..bf86d6363861e 100644 --- a/x-pack/plugins/siem/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/model.ts @@ -14,7 +14,7 @@ import { TimelineType, TimelineStatus, } from '../../../graphql/types'; -import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; +import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts index d4c8f3cf3a2c2..af7ac075468c3 100644 --- a/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts @@ -7,7 +7,7 @@ import { createSelector } from 'reselect'; import { isFromKueryExpressionValid } from '../../../common/lib/keury'; -import { State } from '../../../common/store/reducer'; +import { State } from '../../../common/store/types'; import { TimelineModel } from './model'; import { AutoSavedWarningMsg, TimelineById } from './types'; From 48e3bcdaece8f1af7169a787db902642adaf855e Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 2 Jun 2020 13:53:45 -0700 Subject: [PATCH 56/69] [scripts/type_check] increase memory limit for all x-pack* projects (#68007) Co-authored-by: spalger --- src/dev/typescript/run_type_check_cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index 1417d30484678..5d4cf14c1cd95 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -88,7 +88,7 @@ export function runTypeCheckCli() { } execInProjects(log, projects, process.execPath, (project) => [ - ...(project.name === 'x-pack' ? ['--max-old-space-size=4096'] : []), + ...(project.name.startsWith('x-pack') ? ['--max-old-space-size=4096'] : []), require.resolve('typescript/bin/tsc'), ...['--project', project.tsConfigPath], ...tscArgs, From 728a4b23e6087b0d6a60448b1f84cfe22339647e Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 2 Jun 2020 14:18:37 -0700 Subject: [PATCH 57/69] share core bundle with plugins (#67892) Co-authored-by: spalger --- .../src/optimizer/optimizer_config.ts | 2 +- .../src/worker/webpack.config.ts | 66 +++++++++++-------- src/core/public/index.ts | 4 ++ .../{entry_point.ts => kbn_bootstrap.ts} | 60 +++++++++-------- src/core/public/public.api.md | 3 + .../ui/ui_render/bootstrap/template.js.hbs | 19 ++++-- src/legacy/ui/ui_render/ui_render_mixin.js | 5 +- 7 files changed, 95 insertions(+), 64 deletions(-) rename src/core/public/{entry_point.ts => kbn_bootstrap.ts} (51%) diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 4ed241f3b9b2e..37d8a4f5eb8ae 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -152,7 +152,7 @@ export class OptimizerConfig { new Bundle({ type: 'entry', id: 'core', - entry: './public/entry_point', + entry: './public/index', sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 763f1d515804f..dd003af7dc5e9 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -38,11 +38,32 @@ const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); -const STATIC_BUNDLE_PLUGINS = [ - { id: 'data', dirname: 'data' }, - { id: 'kibanaReact', dirname: 'kibana_react' }, - { id: 'kibanaUtils', dirname: 'kibana_utils' }, - { id: 'esUiShared', dirname: 'es_ui_shared' }, +const SHARED_BUNDLES = [ + { + type: 'entry', + id: 'core', + rootRelativeDir: 'src/core/public', + }, + { + type: 'plugin', + id: 'data', + rootRelativeDir: 'src/plugins/data/public', + }, + { + type: 'plugin', + id: 'kibanaReact', + rootRelativeDir: 'src/plugins/kibana_react/public', + }, + { + type: 'plugin', + id: 'kibanaUtils', + rootRelativeDir: 'src/plugins/kibana_utils/public', + }, + { + type: 'plugin', + id: 'esUiShared', + rootRelativeDir: 'src/plugins/es_ui_shared/public', + }, ]; /** @@ -57,18 +78,8 @@ const STATIC_BUNDLE_PLUGINS = [ * @param request the request for a module from a commonjs require() call or import statement */ function dynamicExternals(bundle: Bundle, context: string, request: string) { - // ignore imports that have loaders defined - if (request.includes('!')) { - return; - } - - // ignore requests that don't include a /{dirname}/public for one of our - // "static" bundles as a cheap way to avoid doing path resolution - // for paths that couldn't possibly resolve to what we're looking for - const reqToStaticBundle = STATIC_BUNDLE_PLUGINS.some((p) => - request.includes(`/${p.dirname}/public`) - ); - if (!reqToStaticBundle) { + // ignore imports that have loaders defined or are not relative seeming + if (request.includes('!') || !request.startsWith('.')) { return; } @@ -76,10 +87,15 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { const rootRelative = normalizePath( Path.relative(bundle.sourceRoot, Path.resolve(context, request)) ); - for (const { id, dirname } of STATIC_BUNDLE_PLUGINS) { - if (rootRelative === `src/plugins/${dirname}/public`) { - return `__kbnBundles__['plugin/${id}']`; + for (const sharedBundle of SHARED_BUNDLES) { + if ( + rootRelative !== sharedBundle.rootRelativeDir || + `${bundle.type}/${bundle.id}` === `${sharedBundle.type}/${sharedBundle.id}` + ) { + continue; } + + return `__kbnBundles__['${sharedBundle.type}/${sharedBundle.id}']`; } // import doesn't match a root public import @@ -112,13 +128,9 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { info.absoluteResourcePath )}${info.query}`, jsonpFunction: `${bundle.id}_bundle_jsonpfunction`, - ...(bundle.type === 'plugin' - ? { - // When the entry point is loaded, assign it's exported `plugin` - // value to a key on the global `__kbnBundles__` object. - library: ['__kbnBundles__', `plugin/${bundle.id}`], - } - : {}), + // When the entry point is loaded, assign it's default export + // to a key on the global `__kbnBundles__` object. + library: ['__kbnBundles__', `${bundle.type}/${bundle.id}`], }, optimization: { diff --git a/src/core/public/index.ts b/src/core/public/index.ts index aa037329b1b6e..bd275ca1d4565 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -35,6 +35,8 @@ * @packageDocumentation */ +import './index.scss'; + import { ChromeBadge, ChromeBrand, @@ -363,3 +365,5 @@ export { UiSettingsState, NavType, }; + +export { __kbnBootstrap__ } from './kbn_bootstrap'; diff --git a/src/core/public/entry_point.ts b/src/core/public/kbn_bootstrap.ts similarity index 51% rename from src/core/public/entry_point.ts rename to src/core/public/kbn_bootstrap.ts index 25180c13ccbd4..caeb95a540de3 100644 --- a/src/core/public/entry_point.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -25,39 +25,41 @@ * src/legacy/ui/ui_bundles/app_entry_template.js */ -import './index.scss'; import { i18n } from '@kbn/i18n'; import { CoreSystem } from './core_system'; -const injectedMetadata = JSON.parse( - document.querySelector('kbn-injected-metadata')!.getAttribute('data')! -); +/** @internal */ +export function __kbnBootstrap__() { + const injectedMetadata = JSON.parse( + document.querySelector('kbn-injected-metadata')!.getAttribute('data')! + ); -/** - * `apmConfig` would be populated with relavant APM RUM agent - * configuration if server is started with `ELASTIC_APM_ACTIVE=true` - */ -if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && injectedMetadata.vars.apmConfig != null) { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { init } = require('@elastic/apm-rum'); - init(injectedMetadata.vars.apmConfig); -} + /** + * `apmConfig` would be populated with relavant APM RUM agent + * configuration if server is started with `ELASTIC_APM_ACTIVE=true` + */ + if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && injectedMetadata.vars.apmConfig != null) { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { init } = require('@elastic/apm-rum'); + init(injectedMetadata.vars.apmConfig); + } -i18n - .load(injectedMetadata.i18n.translationsUrl) - .catch((e) => e) - .then(async (i18nError) => { - const coreSystem = new CoreSystem({ - injectedMetadata, - rootDomElement: document.body, - browserSupportsCsp: !(window as any).__kbnCspNotEnforced__, - }); + i18n + .load(injectedMetadata.i18n.translationsUrl) + .catch((e) => e) + .then(async (i18nError) => { + const coreSystem = new CoreSystem({ + injectedMetadata, + rootDomElement: document.body, + browserSupportsCsp: !(window as any).__kbnCspNotEnforced__, + }); - const setup = await coreSystem.setup(); - if (i18nError && setup) { - setup.fatalErrors.add(i18nError); - } + const setup = await coreSystem.setup(); + if (i18nError && setup) { + setup.fatalErrors.add(i18nError); + } - await coreSystem.start(); - }); + await coreSystem.start(); + }); +} diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index bae0f9a2281cf..74c41d010ca8d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -25,6 +25,9 @@ import { Type } from '@kbn/config-schema'; import { UnregisterCallback } from 'history'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; +// @internal (undocumented) +export function __kbnBootstrap__(): void; + // @public export interface App extends AppBase { appRoute?: string; diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 1453c974c1180..e8f05b46f7061 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -73,12 +73,23 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { } load([ - {{#each jsDependencyPaths}} - '{{this}}', - {{/each}} + {{#each jsDependencyPaths}} + '{{this}}', + {{/each}} ], function () { + {{#unless legacyBundlePath}} + if (!__kbnBundles__ || !__kbnBundles__['entry/core'] || typeof __kbnBundles__['entry/core'].__kbnBootstrap__ !== 'function') { + console.error('entry/core bundle did not load correctly'); + failure(); + } else { + __kbnBundles__['entry/core'].__kbnBootstrap__() + } + {{/unless}} + load([ - '{{entryBundlePath}}', + {{#if legacyBundlePath}} + '{{legacyBundlePath}}', + {{/if}} {{#each styleSheetPaths}} '{{this}}', {{/each}} diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 10847b9928528..b09d4861b343b 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -173,6 +173,7 @@ export function uiRenderMixin(kbnServer, server, config) { `${regularBundlePath}/commons.bundle.js`, ]), + `${regularBundlePath}/core/core.entry.js`, ...kpPluginIds.map( (pluginId) => `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js` ), @@ -199,9 +200,7 @@ export function uiRenderMixin(kbnServer, server, config) { jsDependencyPaths, styleSheetPaths, publicPathMap, - entryBundlePath: isCore - ? `${regularBundlePath}/core/core.entry.js` - : `${regularBundlePath}/${app.getId()}.bundle.js`, + legacyBundlePath: isCore ? undefined : `${regularBundlePath}/${app.getId()}.bundle.js`, }, }); From 5e2a78bb524bdaa1081a52ca71606b197927b622 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Tue, 2 Jun 2020 23:24:06 +0200 Subject: [PATCH 58/69] [test/functional/services] convert dashboard and pie-chart to ts (#66703) Co-authored-by: Elastic Machine --- .../dashboard/{add_panel.js => add_panel.ts} | 34 +++++----- .../{expectations.js => expectations.ts} | 66 ++++++++++--------- .../services/dashboard/{index.js => index.ts} | 0 .../{panel_actions.js => panel_actions.ts} | 59 +++++++++-------- .../{replace_panel.js => replace_panel.ts} | 18 ++--- .../{visualizations.js => visualizations.ts} | 28 ++++++-- test/functional/services/index.ts | 5 +- .../{ => visualizations}/elastic_chart.ts | 2 +- .../visualizations/{index.js => index.ts} | 1 + .../{pie_chart.js => pie_chart.ts} | 37 +++++------ 10 files changed, 137 insertions(+), 113 deletions(-) rename test/functional/services/dashboard/{add_panel.js => add_panel.ts} (88%) rename test/functional/services/dashboard/{expectations.js => expectations.ts} (82%) rename test/functional/services/dashboard/{index.js => index.ts} (100%) rename test/functional/services/dashboard/{panel_actions.js => panel_actions.ts} (80%) rename test/functional/services/dashboard/{replace_panel.js => replace_panel.ts} (86%) rename test/functional/services/dashboard/{visualizations.js => visualizations.ts} (88%) rename test/functional/services/{ => visualizations}/elastic_chart.ts (97%) rename test/functional/services/visualizations/{index.js => index.ts} (93%) rename test/functional/services/visualizations/{pie_chart.js => pie_chart.ts} (78%) diff --git a/test/functional/services/dashboard/add_panel.js b/test/functional/services/dashboard/add_panel.ts similarity index 88% rename from test/functional/services/dashboard/add_panel.js rename to test/functional/services/dashboard/add_panel.ts index 6259203982161..1263501aa9c13 100644 --- a/test/functional/services/dashboard/add_panel.js +++ b/test/functional/services/dashboard/add_panel.ts @@ -17,7 +17,9 @@ * under the License. */ -export function DashboardAddPanelProvider({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function DashboardAddPanelProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -39,7 +41,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { await PageObjects.common.sleep(500); } - async clickAddNewEmbeddableLink(type) { + async clickAddNewEmbeddableLink(type: string) { await testSubjects.click('createNew'); await testSubjects.click(`createNew-${type}`); await testSubjects.missingOrFail(`createNew-${type}`); @@ -50,7 +52,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { await testSubjects.click('savedObjectFinderFilterButton'); } - async toggleFilter(type) { + async toggleFilter(type: string) { log.debug(`DashboardAddPanel.addToFilter(${type})`); await this.waitForListLoading(); await this.toggleFilterPopover(); @@ -61,7 +63,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { async addEveryEmbeddableOnCurrentPage() { log.debug('addEveryEmbeddableOnCurrentPage'); const itemList = await testSubjects.find('savedObjectFinderItemList'); - const embeddableList = []; + const embeddableList: string[] = []; await retry.try(async () => { const embeddableRows = await itemList.findAllByCssSelector('li'); for (let i = 0; i < embeddableRows.length; i++) { @@ -130,7 +132,7 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { await flyout.ensureClosed('dashboardAddPanel'); } - async addEveryVisualization(filter) { + async addEveryVisualization(filter: string) { log.debug('DashboardAddPanel.addEveryVisualization'); await this.ensureAddPanelIsShowing(); await this.toggleFilter('visualization'); @@ -138,16 +140,16 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { await this.filterEmbeddableNames(filter.replace('-', ' ')); } let morePages = true; - const vizList = []; + const vizList: string[][] = []; while (morePages) { vizList.push(await this.addEveryEmbeddableOnCurrentPage()); morePages = await this.clickPagerNextButton(); } await this.closeAddPanel(); - return vizList.reduce((acc, vizList) => [...acc, ...vizList], []); + return vizList.reduce((acc, list) => [...acc, ...list], []); } - async addEverySavedSearch(filter) { + async addEverySavedSearch(filter: string) { log.debug('DashboardAddPanel.addEverySavedSearch'); await this.ensureAddPanelIsShowing(); await this.toggleFilter('search'); @@ -161,20 +163,20 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { morePages = await this.clickPagerNextButton(); } await this.closeAddPanel(); - return searchList.reduce((acc, searchList) => [...acc, ...searchList], []); + return searchList.reduce((acc, list) => [...acc, ...list], []); } - async addSavedSearch(searchName) { + async addSavedSearch(searchName: string) { return this.addEmbeddable(searchName, 'search'); } - async addSavedSearches(searches) { + async addSavedSearches(searches: string[]) { for (const name of searches) { await this.addSavedSearch(name); } } - async addVisualizations(visualizations) { + async addVisualizations(visualizations: string[]) { log.debug('DashboardAddPanel.addVisualizations'); const vizList = []; for (const vizName of visualizations) { @@ -184,11 +186,11 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { return vizList; } - async addVisualization(vizName) { + async addVisualization(vizName: string) { return this.addEmbeddable(vizName, 'visualization'); } - async addEmbeddable(embeddableName, embeddableType) { + async addEmbeddable(embeddableName: string, embeddableType: string) { log.debug( `DashboardAddPanel.addEmbeddable, name: ${embeddableName}, type: ${embeddableType}` ); @@ -201,14 +203,14 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { return embeddableName; } - async filterEmbeddableNames(name) { + async filterEmbeddableNames(name: string) { // The search input field may be disabled while the table is loading so wait for it await this.waitForListLoading(); await testSubjects.setValue('savedObjectFinderSearchInput', name); await this.waitForListLoading(); } - async panelAddLinkExists(name) { + async panelAddLinkExists(name: string) { log.debug(`DashboardAddPanel.panelAddLinkExists(${name})`); await this.ensureAddPanelIsShowing(); await this.filterEmbeddableNames(`"${name}"`); diff --git a/test/functional/services/dashboard/expectations.js b/test/functional/services/dashboard/expectations.ts similarity index 82% rename from test/functional/services/dashboard/expectations.js rename to test/functional/services/dashboard/expectations.ts index 66073e1043b0d..77a441a772d84 100644 --- a/test/functional/services/dashboard/expectations.js +++ b/test/functional/services/dashboard/expectations.ts @@ -18,8 +18,10 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../lib/web_element_wrapper'; -export function DashboardExpectProvider({ getService, getPageObjects }) { +export function DashboardExpectProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -29,7 +31,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { const findTimeout = 2500; return new (class DashboardExpect { - async panelCount(expectedCount) { + async panelCount(expectedCount: number) { log.debug(`DashboardExpect.panelCount(${expectedCount})`); await retry.try(async () => { const panelCount = await PageObjects.dashboard.getPanelCount(); @@ -37,7 +39,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async visualizationsArePresent(vizList) { + async visualizationsArePresent(vizList: string[]) { log.debug('Checking all visualisations are present on dashsboard'); let notLoaded = await PageObjects.dashboard.getNotLoadedVisualizations(vizList); // TODO: Determine issue occasionally preventing 'geo map' from loading @@ -45,7 +47,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { expect(notLoaded).to.be.empty(); } - async selectedLegendColorCount(color, expectedCount) { + async selectedLegendColorCount(color: string, expectedCount: number) { log.debug(`DashboardExpect.selectedLegendColorCount(${color}, ${expectedCount})`); await retry.try(async () => { const selectedLegendColor = await testSubjects.findAll( @@ -56,7 +58,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async docTableFieldCount(expectedCount) { + async docTableFieldCount(expectedCount: number) { log.debug(`DashboardExpect.docTableFieldCount(${expectedCount})`); await retry.try(async () => { const docTableCells = await testSubjects.findAll('docTableField', findTimeout); @@ -64,7 +66,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async fieldSuggestions(expectedFields) { + async fieldSuggestions(expectedFields: string[]) { log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`); const fields = await filterBar.getFilterEditorFields(); expectedFields.forEach((expectedField) => { @@ -72,7 +74,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async legendValuesToExist(legendValues) { + async legendValuesToExist(legendValues: string[]) { log.debug(`DashboardExpect.legendValuesToExist(${legendValues})`); await Promise.all( legendValues.map(async (legend) => { @@ -84,11 +86,11 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { ); } - async textWithinElementsExists(texts, getElementsFn) { + async textWithinElementsExists(texts: string[], getElementsFn: Function) { log.debug(`DashboardExpect.textWithinElementsExists(${texts})`); await retry.try(async () => { - const elements = await getElementsFn(); - const elementTexts = []; + const elements: WebElementWrapper[] = await getElementsFn(); + const elementTexts: string[] = []; await Promise.all( elements.map(async (element) => { elementTexts.push(await element.getVisibleText()); @@ -103,23 +105,23 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async textWithinTestSubjectsExists(texts, selector) { + async textWithinTestSubjectsExists(texts: string[], selector: string) { log.debug(`DashboardExpect.textWithinTestSubjectsExists(${texts})`); log.debug(`textWithinTestSubjectsExists:(${JSON.stringify(texts)},${selector})`); await this.textWithinElementsExists(texts, async () => await testSubjects.findAll(selector)); } - async textWithinCssElementExists(texts, selector) { + async textWithinCssElementExists(texts: string[], selector: string) { log.debug(`DashboardExpect.textWithinCssElementExists(${texts})`); log.debug(`textWithinCssElementExists:(${JSON.stringify(texts)},${selector})`); await this.textWithinElementsExists(texts, async () => await find.allByCssSelector(selector)); } - async textWithinElementsDoNotExist(texts, getElementsFn) { + async textWithinElementsDoNotExist(texts: string[], getElementsFn: Function) { log.debug(`DashboardExpect.textWithinElementsDoNotExist(${texts})`); await retry.try(async () => { - const elements = await getElementsFn(); - const elementTexts = []; + const elements: WebElementWrapper[] = await getElementsFn(); + const elementTexts: string[] = []; await Promise.all( elements.map(async (element) => { elementTexts.push(await element.getVisibleText()); @@ -133,7 +135,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async textWithinCssElementDoNotExist(texts, selector) { + async textWithinCssElementDoNotExist(texts: string[], selector: string) { log.debug(`textWithinCssElementExists:(${JSON.stringify(texts)},${selector})`); await this.textWithinElementsDoNotExist( texts, @@ -141,7 +143,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { ); } - async timelionLegendCount(expectedCount) { + async timelionLegendCount(expectedCount: number) { log.debug(`DashboardExpect.timelionLegendCount(${expectedCount})`); await retry.try(async () => { const flotLegendLabels = await testSubjects.findAll('flotLegendLabel', findTimeout); @@ -160,7 +162,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { expect(tagCloudsHaveContent.indexOf(false)).to.be.greaterThan(-1); } - async tagCloudWithValuesFound(values) { + async tagCloudWithValuesFound(values: string[]) { log.debug(`DashboardExpect.tagCloudWithValuesFound(${values})`); const tagCloudVisualizations = await testSubjects.findAll('tagCloudVisualization'); const matches = await Promise.all( @@ -177,47 +179,47 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { expect(matches.indexOf(true)).to.be.greaterThan(-1); } - async goalAndGuageLabelsExist(labels) { + async goalAndGuageLabelsExist(labels: string[]) { log.debug(`DashboardExpect.goalAndGuageLabelsExist(${labels})`); await this.textWithinCssElementExists(labels, '.chart-label'); } - async metricValuesExist(values) { + async metricValuesExist(values: string[]) { log.debug(`DashboardExpect.metricValuesExist(${values})`); await this.textWithinCssElementExists(values, '.mtrVis__value'); } - async tsvbMetricValuesExist(values) { + async tsvbMetricValuesExist(values: string[]) { log.debug(`DashboardExpect.tsvbMetricValuesExist(${values})`); await this.textWithinTestSubjectsExists(values, 'tsvbMetricValue'); } - async tsvbTopNValuesExist(values) { + async tsvbTopNValuesExist(values: string[]) { log.debug(`DashboardExpect.tsvbTopNValuesExist(${values})`); await this.textWithinTestSubjectsExists(values, 'tsvbTopNValue'); } - async vegaTextsExist(values) { + async vegaTextsExist(values: string[]) { log.debug(`DashboardExpect.vegaTextsExist(${values})`); await this.textWithinCssElementExists(values, '.vgaVis__view text'); } - async vegaTextsDoNotExist(values) { + async vegaTextsDoNotExist(values: string[]) { log.debug(`DashboardExpect.vegaTextsDoNotExist(${values})`); await this.textWithinCssElementDoNotExist(values, '.vgaVis__view text'); } - async tsvbMarkdownWithValuesExists(values) { + async tsvbMarkdownWithValuesExists(values: string[]) { log.debug(`DashboardExpect.tsvbMarkdownWithValuesExists(${values})`); await this.textWithinTestSubjectsExists(values, 'tsvbMarkdown'); } - async markdownWithValuesExists(values) { + async markdownWithValuesExists(values: string[]) { log.debug(`DashboardExpect.markdownWithValuesExists(${values})`); await this.textWithinTestSubjectsExists(values, 'markdownBody'); } - async savedSearchRowCount(expectedCount) { + async savedSearchRowCount(expectedCount: number) { log.debug(`DashboardExpect.savedSearchRowCount(${expectedCount})`); await retry.try(async () => { const savedSearchRows = await testSubjects.findAll( @@ -228,7 +230,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async dataTableRowCount(expectedCount) { + async dataTableRowCount(expectedCount: number) { log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`); await retry.try(async () => { const dataTableRows = await find.allByCssSelector( @@ -239,7 +241,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async seriesElementCount(expectedCount) { + async seriesElementCount(expectedCount: number) { log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`); await retry.try(async () => { const seriesElements = await find.allByCssSelector('.series', findTimeout); @@ -247,7 +249,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async inputControlItemCount(expectedCount) { + async inputControlItemCount(expectedCount: number) { log.debug(`DashboardExpect.inputControlItemCount(${expectedCount})`); await retry.try(async () => { const inputControlItems = await testSubjects.findAll('inputControlItem'); @@ -255,7 +257,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async lineChartPointsCount(expectedCount) { + async lineChartPointsCount(expectedCount: number) { log.debug(`DashboardExpect.lineChartPointsCount(${expectedCount})`); await retry.try(async () => { const points = await find.allByCssSelector('.points', findTimeout); @@ -263,7 +265,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async tsvbTableCellCount(expectedCount) { + async tsvbTableCellCount(expectedCount: number) { log.debug(`DashboardExpect.tsvbTableCellCount(${expectedCount})`); await retry.try(async () => { const tableCells = await testSubjects.findAll('tvbTableVis__value', findTimeout); diff --git a/test/functional/services/dashboard/index.js b/test/functional/services/dashboard/index.ts similarity index 100% rename from test/functional/services/dashboard/index.js rename to test/functional/services/dashboard/index.ts diff --git a/test/functional/services/dashboard/panel_actions.js b/test/functional/services/dashboard/panel_actions.ts similarity index 80% rename from test/functional/services/dashboard/panel_actions.js rename to test/functional/services/dashboard/panel_actions.ts index b155d747f3b93..c9a5dcfba32b1 100644 --- a/test/functional/services/dashboard/panel_actions.js +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,9 @@ * under the License. */ +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../lib/web_element_wrapper'; + const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel'; const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel'; const REPLACE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-replacePanel'; @@ -26,13 +29,13 @@ const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_P const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; -export function DashboardPanelActionsProvider({ getService, getPageObjects }) { +export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['header', 'common']); return new (class DashboardPanelActions { - async findContextMenu(parent) { + async findContextMenu(parent?: WebElementWrapper) { return parent ? await testSubjects.findDescendant(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ, parent) : await testSubjects.find(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); @@ -43,7 +46,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { return await testSubjects.exists(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); } - async toggleContextMenu(parent) { + async toggleContextMenu(parent?: WebElementWrapper) { log.debug('toggleContextMenu'); await (parent ? parent.moveMouseTo() : testSubjects.moveMouseTo('dashboardPanelTitle')); const toggleMenuItem = await this.findContextMenu(parent); @@ -54,7 +57,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.existOrFail('embeddablePanelContextMenuOpen'); } - async openContextMenu(parent) { + async openContextMenu(parent?: WebElementWrapper) { log.debug(`openContextMenu(${parent}`); await this.toggleContextMenu(parent); await this.expectContextMenuToBeOpen(); @@ -77,43 +80,45 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.click(REMOVE_PANEL_DATA_TEST_SUBJ); } - async removePanelByTitle(title) { + async removePanelByTitle(title: string) { const header = await this.getPanelHeading(title); await this.openContextMenu(header); await testSubjects.click(REMOVE_PANEL_DATA_TEST_SUBJ); } - async customizePanel(parent) { + async customizePanel(parent?: WebElementWrapper) { await this.openContextMenu(parent); await testSubjects.click(CUSTOMIZE_PANEL_DATA_TEST_SUBJ); } - async replacePanelByTitle(title) { + async replacePanelByTitle(title?: string) { log.debug(`replacePanel(${title})`); - let panelOptions = null; if (title) { - panelOptions = await this.getPanelHeading(title); + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { + await this.openContextMenu(); } - await this.openContextMenu(panelOptions); await testSubjects.click(REPLACE_PANEL_DATA_TEST_SUBJ); } - async clonePanelByTitle(title) { + async clonePanelByTitle(title?: string) { log.debug(`clonePanel(${title})`); - let panelOptions = null; if (title) { - panelOptions = await this.getPanelHeading(title); + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { + await this.openContextMenu(); } - await this.openContextMenu(panelOptions); await testSubjects.click(CLONE_PANEL_DATA_TEST_SUBJ); } - async openInspectorByTitle(title) { + async openInspectorByTitle(title: string) { const header = await this.getPanelHeading(title); await this.openInspector(header); } - async openInspector(parent) { + async openInspector(parent: WebElementWrapper) { await this.openContextMenu(parent); await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); } @@ -163,7 +168,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.existOrFail(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); } - async getPanelHeading(title) { + async getPanelHeading(title: string) { return await testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); } @@ -171,13 +176,14 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.click('customizePanelHideTitle'); } - async toggleHidePanelTitle(originalTitle) { + async toggleHidePanelTitle(originalTitle: string) { log.debug(`hidePanelTitle(${originalTitle})`); - let panelOptions = null; if (originalTitle) { - panelOptions = await this.getPanelHeading(originalTitle); + const panelOptions = await this.getPanelHeading(originalTitle); + await this.customizePanel(panelOptions); + } else { + await this.customizePanel(); } - await this.customizePanel(panelOptions); await this.clickHidePanelTitleToggle(); await testSubjects.click('saveNewTitleButton'); } @@ -188,18 +194,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { * @param originalTitle - optional to specify which panel to change the title on. * @return {Promise} */ - async setCustomPanelTitle(customTitle, originalTitle) { + async setCustomPanelTitle(customTitle: string, originalTitle?: string) { log.debug(`setCustomPanelTitle(${customTitle}, ${originalTitle})`); - let panelOptions = null; if (originalTitle) { - panelOptions = await this.getPanelHeading(originalTitle); + const panelOptions = await this.getPanelHeading(originalTitle); + await this.customizePanel(panelOptions); + } else { + await this.customizePanel(); } - await this.customizePanel(panelOptions); await testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle); await testSubjects.click('saveNewTitleButton'); } - async resetCustomPanelTitle(panel) { + async resetCustomPanelTitle(panel: WebElementWrapper) { log.debug('resetCustomPanelTitle'); await this.customizePanel(panel); await testSubjects.click('resetCustomEmbeddablePanelTitle'); diff --git a/test/functional/services/dashboard/replace_panel.js b/test/functional/services/dashboard/replace_panel.ts similarity index 86% rename from test/functional/services/dashboard/replace_panel.js rename to test/functional/services/dashboard/replace_panel.ts index faa4d404442d7..d1cb4e5e697a1 100644 --- a/test/functional/services/dashboard/replace_panel.js +++ b/test/functional/services/dashboard/replace_panel.ts @@ -17,7 +17,9 @@ * under the License. */ -export function DashboardReplacePanelProvider({ getService }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function DashboardReplacePanelProvider({ getService }: FtrProviderContext) { const log = getService('log'); const testSubjects = getService('testSubjects'); const flyout = getService('flyout'); @@ -28,7 +30,7 @@ export function DashboardReplacePanelProvider({ getService }) { await testSubjects.click('savedObjectFinderFilterButton'); } - async toggleFilter(type) { + async toggleFilter(type: string) { log.debug(`DashboardReplacePanel.replaceToFilter(${type})`); await this.waitForListLoading(); await this.toggleFilterPopover(); @@ -57,21 +59,21 @@ export function DashboardReplacePanelProvider({ getService }) { await flyout.ensureClosed('dashboardReplacePanel'); } - async replaceSavedSearch(searchName) { + async replaceSavedSearch(searchName: string) { return this.replaceEmbeddable(searchName, 'search'); } - async replaceSavedSearches(searches) { + async replaceSavedSearches(searches: string[]) { for (const name of searches) { await this.replaceSavedSearch(name); } } - async replaceVisualization(vizName) { + async replaceVisualization(vizName: string) { return this.replaceEmbeddable(vizName, 'visualization'); } - async replaceEmbeddable(embeddableName, embeddableType) { + async replaceEmbeddable(embeddableName: string, embeddableType: string) { log.debug( `DashboardReplacePanel.replaceEmbeddable, name: ${embeddableName}, type: ${embeddableType}` ); @@ -86,14 +88,14 @@ export function DashboardReplacePanelProvider({ getService }) { return embeddableName; } - async filterEmbeddableNames(name) { + async filterEmbeddableNames(name: string) { // The search input field may be disabled while the table is loading so wait for it await this.waitForListLoading(); await testSubjects.setValue('savedObjectFinderSearchInput', name); await this.waitForListLoading(); } - async panelReplaceLinkExists(name) { + async panelReplaceLinkExists(name: string) { log.debug(`DashboardReplacePanel.panelReplaceLinkExists(${name})`); await this.ensureReplacePanelIsShowing(); await this.filterEmbeddableNames(`"${name}"`); diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.ts similarity index 88% rename from test/functional/services/dashboard/visualizations.js rename to test/functional/services/dashboard/visualizations.ts index 676e4c384fe36..10747658d8c9b 100644 --- a/test/functional/services/dashboard/visualizations.js +++ b/test/functional/services/dashboard/visualizations.ts @@ -17,7 +17,9 @@ * under the License. */ -export function DashboardVisualizationProvider({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function DashboardVisualizationProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const find = getService('find'); const retry = getService('retry'); @@ -34,7 +36,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { ]); return new (class DashboardVisualizations { - async createAndAddTSVBVisualization(name) { + async createAndAddTSVBVisualization(name: string) { log.debug(`createAndAddTSVBVisualization(${name})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { @@ -46,7 +48,15 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { await PageObjects.visualize.saveVisualizationExpectSuccess(name); } - async createSavedSearch({ name, query, fields }) { + async createSavedSearch({ + name, + query, + fields, + }: { + name: string; + query?: string; + fields?: string[]; + }) { log.debug(`createSavedSearch(${name})`); await PageObjects.header.clickDiscover(); @@ -68,7 +78,15 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { await testSubjects.exists('saveSearchSuccess'); } - async createAndAddSavedSearch({ name, query, fields }) { + async createAndAddSavedSearch({ + name, + query, + fields, + }: { + name: string; + query?: string; + fields?: string[]; + }) { log.debug(`createAndAddSavedSearch(${name})`); await this.createSavedSearch({ name, query, fields }); @@ -106,7 +124,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { } } - async createAndAddMarkdown({ name, markdown }) { + async createAndAddMarkdown({ name, markdown }: { name: string; markdown: string }) { log.debug(`createAndAddMarkdown(${markdown})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index cbb0c6790dbe9..7891a6b00f729 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -35,10 +35,8 @@ import { DashboardExpectProvider, DashboardPanelActionsProvider, DashboardVisualizationProvider, - // @ts-ignore not TS yet } from './dashboard'; import { DocTableProvider } from './doc_table'; -import { ElasticChartProvider } from './elastic_chart'; import { EmbeddingProvider } from './embedding'; import { FilterBarProvider } from './filter_bar'; import { FlyoutProvider } from './flyout'; @@ -49,8 +47,7 @@ import { RemoteProvider } from './remote'; import { RenderableProvider } from './renderable'; import { TableProvider } from './table'; import { ToastsProvider } from './toasts'; -// @ts-ignore not TS yet -import { PieChartProvider } from './visualizations'; +import { PieChartProvider, ElasticChartProvider } from './visualizations'; import { ListingTableProvider } from './listing_table'; import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; import { KibanaSupertestProvider } from './supertest'; diff --git a/test/functional/services/elastic_chart.ts b/test/functional/services/visualizations/elastic_chart.ts similarity index 97% rename from test/functional/services/elastic_chart.ts rename to test/functional/services/visualizations/elastic_chart.ts index 1c3071ac01587..3c454f0a88e24 100644 --- a/test/functional/services/elastic_chart.ts +++ b/test/functional/services/visualizations/elastic_chart.ts @@ -18,7 +18,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export function ElasticChartProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/services/visualizations/index.js b/test/functional/services/visualizations/index.ts similarity index 93% rename from test/functional/services/visualizations/index.js rename to test/functional/services/visualizations/index.ts index 1cc9d40abeab6..1da1691b07c2a 100644 --- a/test/functional/services/visualizations/index.js +++ b/test/functional/services/visualizations/index.ts @@ -18,3 +18,4 @@ */ export { PieChartProvider } from './pie_chart'; +export { ElasticChartProvider } from './elastic_chart'; diff --git a/test/functional/services/visualizations/pie_chart.js b/test/functional/services/visualizations/pie_chart.ts similarity index 78% rename from test/functional/services/visualizations/pie_chart.js rename to test/functional/services/visualizations/pie_chart.ts index edabc7ce989c0..66f32d246b31f 100644 --- a/test/functional/services/visualizations/pie_chart.js +++ b/test/functional/services/visualizations/pie_chart.ts @@ -18,8 +18,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export function PieChartProvider({ getService }) { +export function PieChartProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const config = getService('config'); @@ -29,7 +30,7 @@ export function PieChartProvider({ getService }) { const defaultFindTimeout = config.get('timeouts.find'); return new (class PieChart { - async filterOnPieSlice(name) { + async filterOnPieSlice(name?: string) { log.debug(`PieChart.filterOnPieSlice(${name})`); if (name) { await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`); @@ -43,27 +44,27 @@ export function PieChartProvider({ getService }) { } } - async filterByLegendItem(label) { + async filterByLegendItem(label: string) { log.debug(`PieChart.filterByLegendItem(${label})`); await testSubjects.click(`legend-${label}`); await testSubjects.click(`legend-${label}-filterIn`); } - async getPieSlice(name) { + async getPieSlice(name: string) { return await testSubjects.find(`pieSlice-${name.split(' ').join('-')}`); } - async getAllPieSlices(name) { + async getAllPieSlices(name: string) { return await testSubjects.findAll(`pieSlice-${name.split(' ').join('-')}`); } - async getPieSliceStyle(name) { + async getPieSliceStyle(name: string) { log.debug(`VisualizePage.getPieSliceStyle(${name})`); const pieSlice = await this.getPieSlice(name); return await pieSlice.getAttribute('style'); } - async getAllPieSliceStyles(name) { + async getAllPieSliceStyles(name: string) { log.debug(`VisualizePage.getAllPieSliceStyles(${name})`); const pieSlices = await this.getAllPieSlices(name); return await Promise.all( @@ -73,12 +74,10 @@ export function PieChartProvider({ getService }) { async getPieChartData() { const chartTypes = await find.allByCssSelector('path.slice', defaultFindTimeout * 2); - - const getChartTypesPromises = chartTypes.map(async (chart) => await chart.getAttribute('d')); - return await Promise.all(getChartTypesPromises); + return await Promise.all(chartTypes.map(async (chart) => await chart.getAttribute('d'))); } - async expectPieChartTableData(expectedTableData) { + async expectPieChartTableData(expectedTableData: Array<[]>) { await inspector.open(); await inspector.setTablePageSize(50); await inspector.expectTableData(expectedTableData); @@ -86,22 +85,18 @@ export function PieChartProvider({ getService }) { async getPieChartLabels() { const chartTypes = await find.allByCssSelector('path.slice', defaultFindTimeout * 2); - - const getChartTypesPromises = chartTypes.map( - async (chart) => await chart.getAttribute('data-label') + return await Promise.all( + chartTypes.map(async (chart) => await chart.getAttribute('data-label')) ); - return await Promise.all(getChartTypesPromises); } async getPieSliceCount() { log.debug('PieChart.getPieSliceCount'); - return await retry.try(async () => { - const slices = await find.allByCssSelector('svg > g > g.arcs > path.slice', 2500); - return slices.length; - }); + const slices = await find.allByCssSelector('svg > g > g.arcs > path.slice'); + return slices.length; } - async expectPieSliceCount(expectedCount) { + async expectPieSliceCount(expectedCount: number) { log.debug(`PieChart.expectPieSliceCount(${expectedCount})`); await retry.try(async () => { const slicesCount = await this.getPieSliceCount(); @@ -109,7 +104,7 @@ export function PieChartProvider({ getService }) { }); } - async expectPieChartLabels(expectedLabels) { + async expectPieChartLabels(expectedLabels: string[]) { log.debug(`PieChart.expectPieChartLabels(${expectedLabels.join(',')})`); await retry.try(async () => { const pieData = await this.getPieChartLabels(); From 0247df4cfd689630b174f2f094c35df0a40d16ef Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Tue, 2 Jun 2020 17:46:21 -0400 Subject: [PATCH 59/69] New Kibana app link order (#66320) --- src/core/public/chrome/nav_links/to_nav_link.ts | 4 +--- src/core/public/chrome/ui/header/nav_link.tsx | 7 ++++++- src/plugins/dashboard/public/plugin.tsx | 2 +- src/plugins/discover/public/plugin.ts | 2 +- src/plugins/visualize/public/plugin.ts | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index b8f97f9ddc005..2dedbfd5f36ac 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -42,9 +42,7 @@ export function toNavLink( legacy: isLegacyApp(app), baseUrl, ...(isLegacyApp(app) - ? { - href: url && !url.startsWith(app.subUrlBase!) ? url : baseUrl, - } + ? {} : { href: url, url, diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c09b15fac9bdb..969b6728e0263 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -55,7 +55,12 @@ export function createEuiListItem({ navigateToApp, dataTestSubj, }: Props) { - const { legacy, active, id, title, disabled, euiIconType, icon, tooltip, href } = link; + const { legacy, active, id, title, disabled, euiIconType, icon, tooltip } = link; + let { href } = link; + + if (legacy) { + href = link.url && !active ? link.url : link.baseUrl; + } return { label: tooltip ?? title, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 08740b21f39a4..0de3982039928 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -228,7 +228,7 @@ export class DashboardPlugin const app: App = { id: DashboardConstants.DASHBOARDS_ID, title: 'Dashboard', - order: -1001, + order: 2500, euiIconType: 'dashboardApp', defaultPath: `#${DashboardConstants.LANDING_PAGE_PATH}`, updater$: this.appStateUpdater, diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 5a031872913c0..4323e3d8deda4 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -188,7 +188,7 @@ export class DiscoverPlugin id: 'discover', title: 'Discover', updater$: this.appStateUpdater.asObservable(), - order: -1004, + order: 1000, euiIconType: 'discoverApp', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 8a104dc51feb4..8a05adc18964a 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -102,7 +102,7 @@ export class VisualizePlugin core.application.register({ id: 'visualize', title: 'Visualize', - order: -1002, + order: 8000, euiIconType: 'visualizeApp', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, From eab74f400522faf4856b59dedddeea07c2ffe45f Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 2 Jun 2020 16:08:41 -0700 Subject: [PATCH 60/69] [Ingest Manager] Change agent config YAML view to flyout (#67918) * Consolidate context menu/row action components and usage, and some export/imports * Missed export * Move agent config yaml tab to flyout, add and consolidate agent config action menus * Fix i18n * Add download config YAML endpoint and link from flyout, add common config->yaml service * Actually remove the tab lol * Fix i18n --- .../ingest_manager/common/constants/routes.ts | 1 + .../common/services/config_to_yaml.ts | 39 +++++ .../ingest_manager/common/services/index.ts | 1 + .../ingest_manager/common/services/routes.ts | 7 + .../components/context_menu_actions.tsx | 68 +++++++++ .../ingest_manager/components/index.ts | 3 + .../components/table_row_actions_nested.tsx | 38 ----- .../ingest_manager/constants/page_paths.ts | 1 - .../ingest_manager/hooks/index.ts | 1 + .../hooks/use_request/agent_config.ts | 3 +- .../agent_config/components/actions_menu.tsx | 69 +++++++++ .../components/config_yaml_flyout.tsx | 99 +++++++++++++ .../sections/agent_config/components/index.ts | 4 +- .../components/table_row_actions.tsx | 38 ----- .../datasources/datasources_table.tsx | 10 +- .../details_page/components/index.ts | 2 + .../details_page/components/yaml/index.tsx | 56 -------- .../agent_config/details_page/index.tsx | 38 +++-- .../sections/agent_config/list_page/index.tsx | 68 ++------- .../components/data_stream_row_actions.tsx | 9 +- .../components/actions_menu.tsx | 99 ++++++------- .../sections/fleet/agent_list_page/index.tsx | 135 ++++++++---------- .../ingest_manager/services/index.ts | 1 + .../ingest_manager/types/index.ts | 1 + .../server/routes/agent_config/handlers.ts | 37 ++++- .../server/routes/agent_config/index.ts | 11 ++ .../ingest_manager/server/services/index.ts | 2 +- .../server/types/rest_spec/agent_config.ts | 3 + .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 30 files changed, 481 insertions(+), 373 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_yaml_flyout.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index abb266da9f066..3309d8497f4c5 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -46,6 +46,7 @@ export const AGENT_CONFIG_API_ROUTES = { UPDATE_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}`, DELETE_PATTERN: `${AGENT_CONFIG_API_ROOT}/delete`, FULL_INFO_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}/full`, + FULL_INFO_DOWNLOAD_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}/download`, }; // Output API routes diff --git a/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts b/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts new file mode 100644 index 0000000000000..9dfd76b9ddd21 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { safeDump } from 'js-yaml'; +import { FullAgentConfig } from '../types'; + +const CONFIG_KEYS_ORDER = [ + 'id', + 'name', + 'revision', + 'type', + 'outputs', + 'settings', + 'datasources', + 'enabled', + 'package', + 'input', +]; + +export const configToYaml = (config: FullAgentConfig): string => { + return safeDump(config, { + skipInvalid: true, + sortKeys: (keyA: string, keyB: string) => { + const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); + const indexB = CONFIG_KEYS_ORDER.indexOf(keyB); + if (indexA >= 0 && indexB < 0) { + return -1; + } + + if (indexA < 0 && indexB >= 0) { + return 1; + } + + return indexA - indexB; + }, + }); +}; diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index 91dbbdd515c3e..c595c9a52f66f 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -8,5 +8,6 @@ import * as AgentStatusKueryHelper from './agent_status'; export * from './routes'; export { packageToConfigDatasourceInputs, packageToConfigDatasource } from './package_to_config'; export { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; +export { configToYaml } from './config_to_yaml'; export { AgentStatusKueryHelper }; export { decodeCloudId } from './decode_cloud_id'; diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 20d040ac6eaee..3fc990ea9d70c 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -90,6 +90,13 @@ export const agentConfigRouteService = { getInfoFullPath: (agentConfigId: string) => { return AGENT_CONFIG_API_ROUTES.FULL_INFO_PATTERN.replace('{agentConfigId}', agentConfigId); }, + + getInfoFullDownloadPath: (agentConfigId: string) => { + return AGENT_CONFIG_API_ROUTES.FULL_INFO_DOWNLOAD_PATTERN.replace( + '{agentConfigId}', + agentConfigId + ); + }, }; export const dataStreamRouteService = { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx new file mode 100644 index 0000000000000..8a9f0553895a1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanel, + EuiPopover, + EuiButton, +} from '@elastic/eui'; +import { EuiButtonProps } from '@elastic/eui/src/components/button/button'; +import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu'; +import { EuiContextMenuPanelProps } from '@elastic/eui/src/components/context_menu/context_menu_panel'; + +type Props = { + button?: { + props: EuiButtonProps; + children: JSX.Element; + }; +} & ( + | { + items: EuiContextMenuPanelProps['items']; + } + | { + panels: EuiContextMenuProps['panels']; + } +); + +export const ContextMenuActions = React.memo(({ button, ...props }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + {button.children} + + ) : ( + + ) + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + {'items' in props ? ( + + ) : ( + + )} + + ); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts index b0b4e79cece79..93bc0645c7eee 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts @@ -7,4 +7,7 @@ export { Loading } from './loading'; export { Error } from './error'; export { Header, HeaderProps } from './header'; export { AlphaMessaging } from './alpha_messaging'; +export { PackageIcon } from './package_icon'; +export { ContextMenuActions } from './context_menu_actions'; +export { SearchBar } from './search_bar'; export * from './settings_flyout'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx deleted file mode 100644 index 56f010e2fa774..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useState } from 'react'; -import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu'; - -export const TableRowActionsNested = React.memo<{ panels: EuiContextMenuProps['panels'] }>( - ({ panels }) => { - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); - - return ( - - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - - - ); - } -); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts index 73771fa3cb343..5ef7f45faec48 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -43,7 +43,6 @@ export const PAGE_ROUTING_PATHS = { configurations: '/configs', configurations_list: '/configs', configuration_details: '/configs/:configId/:tabId?', - configuration_details_yaml: '/configs/:configId/yaml', configuration_details_settings: '/configs/:configId/settings', add_datasource_from_configuration: '/configs/:configId/add-datasource', add_datasource_from_integration: '/integrations/:pkgkey/add-datasource', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index a752ad2a8912b..31a511f2d79dc 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -10,6 +10,7 @@ export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; +export { useKibanaLink } from './use_kibana_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; export { useDebounce } from './use_debounce'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index f80c468677f48..45ca6047b0d96 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts @@ -14,6 +14,7 @@ import { agentConfigRouteService } from '../../services'; import { GetAgentConfigsResponse, GetOneAgentConfigResponse, + GetFullAgentConfigResponse, CreateAgentConfigRequest, CreateAgentConfigResponse, UpdateAgentConfigRequest, @@ -39,7 +40,7 @@ export const useGetOneAgentConfig = (agentConfigId: string | undefined) => { }; export const useGetOneAgentConfigFull = (agentConfigId: string) => { - return useRequest({ + return useRequest({ path: agentConfigRouteService.getInfoFullPath(agentConfigId), method: 'get', }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx new file mode 100644 index 0000000000000..78ed228012691 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; +import { useCapabilities, useLink } from '../../../hooks'; +import { ContextMenuActions } from '../../../components'; +import { ConfigYamlFlyout } from './config_yaml_flyout'; + +export const AgentConfigActionMenu = memo<{ configId: string; fullButton?: boolean }>( + ({ configId, fullButton = false }) => { + const { getHref } = useLink(); + const hasWriteCapabilities = useCapabilities().write; + const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState(false); + return ( + <> + {isYamlFlyoutOpen ? ( + + setIsYamlFlyoutOpen(false)} /> + + ) : null} + + ), + } + : undefined + } + items={[ + setIsYamlFlyoutOpen(!isYamlFlyoutOpen)} + key="viewConfig" + > + + , + + + , + ]} + /> + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_yaml_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_yaml_flyout.tsx new file mode 100644 index 0000000000000..6cf60fe1dc507 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_yaml_flyout.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { useGetOneAgentConfigFull, useGetOneAgentConfig, useCore } from '../../../hooks'; +import { Loading } from '../../../components'; +import { configToYaml, agentConfigRouteService } from '../../../services'; + +const FlyoutBody = styled(EuiFlyoutBody)` + .euiFlyoutBody__overflowContent { + padding: 0; + } +`; + +export const ConfigYamlFlyout = memo<{ configId: string; onClose: () => void }>( + ({ configId, onClose }) => { + const core = useCore(); + const { isLoading: isLoadingYaml, data: yamlData } = useGetOneAgentConfigFull(configId); + const { data: configData } = useGetOneAgentConfig(configId); + + const body = + isLoadingYaml && !yamlData ? ( + + ) : ( + + {configToYaml(yamlData!.item)} + + ); + + const downloadLink = core.http.basePath.prepend( + agentConfigRouteService.getInfoFullDownloadPath(configId) + ); + + return ( + + + +

    + {configData?.item ? ( + + ) : ( + + )} +

    +
    +
    + {body} + + + + + + + + + + + + + + +
    + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts index c1811b99588a8..f3ec15e0f477d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts @@ -3,8 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - export { AgentConfigForm, agentConfigFormValidation } from './config_form'; export { AgentConfigDeleteProvider } from './config_delete_provider'; +export { DatasourceDeleteProvider } from './datasource_delete_provider'; export { LinkedAgentCount } from './linked_agent_count'; export { ConfirmDeployConfigModal } from './confirm_deploy_modal'; +export { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; +export { AgentConfigActionMenu } from './actions_menu'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx deleted file mode 100644 index 2f9a11ef76704..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/table_row_actions.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useState } from 'react'; -import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { EuiContextMenuPanelProps } from '@elastic/eui/src/components/context_menu/context_menu_panel'; - -export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['items'] }>( - ({ items }) => { - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); - - return ( - - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - - - ); - } -); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 316b7eed491b9..01505fcf4c65e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -17,12 +17,10 @@ import { EuiFlexItem, } from '@elastic/eui'; import { AgentConfig, Datasource } from '../../../../../types'; -import { TableRowActions } from '../../../components/table_row_actions'; -import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; +import { PackageIcon, ContextMenuActions } from '../../../../../components'; +import { DatasourceDeleteProvider, DangerEuiContextMenuItem } from '../../../components'; import { useCapabilities, useLink } from '../../../../../hooks'; -import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; -import { useConfigRefresh } from '../../hooks/use_config'; -import { PackageIcon } from '../../../../../components/package_icon'; +import { useConfigRefresh } from '../../hooks'; interface InMemoryDatasource extends Datasource { streams: { total: number; enabled: number }; @@ -197,7 +195,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ actions: [ { render: (datasource: InMemoryDatasource) => ( - (({ config }) => { - const fullConfigRequest = useGetOneAgentConfigFull(config.id); - - if (fullConfigRequest.isLoading && !fullConfigRequest.data) { - return ; - } - - return ( - - - - {dump(fullConfigRequest.data.item, { - sortKeys: (keyA: string, keyB: string) => { - const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); - const indexB = CONFIG_KEYS_ORDER.indexOf(keyB); - if (indexA >= 0 && indexB < 0) { - return -1; - } - - if (indexA < 0 && indexB >= 0) { - return 1; - } - - return indexA - indexB; - }, - })} - - - - ); -}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 3f886645b5339..1dd7e660deaa9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -27,10 +27,8 @@ import { useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; -import { LinkedAgentCount } from '../components'; -import { ConfigDatasourcesView } from './components/datasources'; -import { ConfigYamlView } from './components/yaml'; -import { ConfigSettingsView } from './components/settings'; +import { LinkedAgentCount, AgentConfigActionMenu } from '../components'; +import { ConfigDatasourcesView, ConfigSettingsView } from './components'; const Divider = styled.div` width: 0; @@ -147,21 +145,31 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { )) || '', }, + { isDivider: true }, + { + content: agentConfig && , + }, ].map((item, index) => ( {item.isDivider ?? false ? ( - ) : ( + ) : item.label ? ( - {item.label} - {item.content} + + {item.label} + + + {item.content} + + ) : ( + item.content )} ))} ), - [agentConfig, agentStatus] + [agentConfig, configId, agentStatus] ); const headerTabs = useMemo(() => { @@ -174,14 +182,6 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { href: getHref('configuration_details', { configId, tabId: 'datasources' }), isSelected: tabId === '' || tabId === 'datasources', }, - { - id: 'yaml', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', { - defaultMessage: 'YAML', - }), - href: getHref('configuration_details', { configId, tabId: 'yaml' }), - isSelected: tabId === 'yaml', - }, { id: 'settings', name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', { @@ -254,12 +254,6 @@ const AgentConfigDetailsContent: React.FunctionComponent<{ agentConfig: AgentCon useBreadcrumbs('configuration_details', { configName: agentConfig.name }); return ( - { - return ; - }} - /> { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 5b4066e53f0c8..0d43d8856c2fb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { CSSProperties, memo, useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiSpacer, EuiText, @@ -16,7 +16,6 @@ import { EuiTableActionsColumnType, EuiTableFieldDataColumnType, EuiTextColor, - EuiContextMenuItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; @@ -33,16 +32,9 @@ import { useUrlParams, useBreadcrumbs, } from '../../../hooks'; +import { SearchBar } from '../../../components'; +import { LinkedAgentCount, AgentConfigActionMenu } from '../components'; import { CreateAgentConfigFlyout } from './components'; -import { SearchBar } from '../../../components/search_bar'; -import { LinkedAgentCount } from '../components'; -import { TableRowActions } from '../components/table_row_actions'; - -const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', -}); const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => ( ( ); -const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( - ({ config, onDelete }) => { - const { getHref } = useLink(); - const hasWriteCapabilities = useCapabilities().write; - - return ( - - - , - - - - , - // - // - // , - ]} - /> - ); - } -); - export const AgentConfigListPage: React.FunctionComponent<{}> = () => { useBreadcrumbs('configurations_list'); const { getHref, getPath } = useLink(); @@ -172,10 +122,10 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { width: '20%', render: (name: string, agentConfig: AgentConfig) => ( - + {name || agentConfig.id} @@ -201,7 +151,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { width: '35%', truncateText: true, render: (description: AgentConfig['description']) => ( - + {description} ), @@ -239,9 +189,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { }), actions: [ { - render: (config: AgentConfig) => ( - sendRequest()} /> - ), + render: (config: AgentConfig) => , }, ], }, @@ -253,7 +201,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { } return cols; - }, [getHref, isFleetEnabled, sendRequest]); + }, [getHref, isFleetEnabled]); const createAgentConfigButton = useMemo( () => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx index b87ae4c4561ff..cdc4f1c63a11d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx @@ -5,12 +5,11 @@ */ import React, { memo } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useKibanaLink } from '../../../../hooks/use_kibana_link'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DataStream } from '../../../../types'; -import { TableRowActionsNested } from '../../../../components/table_row_actions_nested'; +import { useKibanaLink } from '../../../../hooks'; +import { ContextMenuActions } from '../../../../components'; export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastream }) => { const { dashboards } = datastream; @@ -78,5 +77,5 @@ export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastre }); } - return ; + return ; }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx index 34a7ad8eb1efc..27e17f6b3df61 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx @@ -3,81 +3,70 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useState, useCallback } from 'react'; -import { EuiButton, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import React, { memo, useState } from 'react'; +import { EuiPortal, EuiContextMenuItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { useCapabilities } from '../../../../hooks'; -import { useAgentRefresh } from '../hooks'; +import { ContextMenuActions } from '../../../../components'; import { AgentUnenrollProvider, AgentReassignConfigFlyout } from '../../components'; +import { useAgentRefresh } from '../hooks'; export const AgentDetailsActionMenu: React.FunctionComponent<{ agent: Agent; }> = memo(({ agent }) => { const hasWriteCapabilites = useCapabilities().write; const refreshAgent = useAgentRefresh(); - const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsActionsPopoverOpen(false), [ - setIsActionsPopoverOpen, - ]); - const handleToggleMenu = useCallback(() => setIsActionsPopoverOpen(!isActionsPopoverOpen), [ - isActionsPopoverOpen, - ]); const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); return ( <> {isReassignFlyoutOpen && ( - setIsReassignFlyoutOpen(false)} /> + + setIsReassignFlyoutOpen(false)} /> + )} - + - - } - isOpen={isActionsPopoverOpen} - closePopover={handleCloseMenu} - > - { - handleCloseMenu(); - setIsReassignFlyoutOpen(true); - }} - key="reassignConfig" - > - - , - - {(unenrollAgentsPrompt) => ( - { - unenrollAgentsPrompt([agent.id], 1, refreshAgent); - }} - > - - - )} - , - ]} - /> - + ), + }} + items={[ + { + setIsReassignFlyoutOpen(true); + }} + key="reassignConfig" + > + + , + + {(unenrollAgentsPrompt) => ( + { + unenrollAgentsPrompt([agent.id], 1, refreshAgent); + }} + > + + + )} + , + ]} + /> ); }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index d5b8b393e7ed9..281a8d3a9745c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -17,10 +17,9 @@ import { EuiPopover, EuiSpacer, EuiText, - EuiButtonIcon, - EuiContextMenuPanel, EuiContextMenuItem, EuiIcon, + EuiPortal, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; @@ -36,12 +35,10 @@ import { useLink, useBreadcrumbs, } from '../../../hooks'; -import { AgentReassignConfigFlyout } from '../components'; -import { SearchBar } from '../../../components/search_bar'; -import { AgentHealth } from '../components/agent_health'; -import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; +import { SearchBar, ContextMenuActions } from '../../../components'; import { AgentStatusKueryHelper } from '../../../services'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AgentReassignConfigFlyout, AgentHealth, AgentUnenrollProvider } from '../components'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -76,73 +73,53 @@ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refre ({ agent, refresh, onReassignClick }) => { const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); return ( - - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - - - , - { - handleCloseMenu(); - onReassignClick(); - }} - key="reassignConfig" - > - - , + + + , + { + onReassignClick(); + }} + key="reassignConfig" + > + + , - - {(unenrollAgentsPrompt) => ( - { - unenrollAgentsPrompt([agent.id], 1, () => { - refresh(); - }); - }} - > - - - )} - , - ]} - /> - + + {(unenrollAgentsPrompt) => ( + { + unenrollAgentsPrompt([agent.id], 1, () => { + refresh(); + }); + }} + > + + + )} + , + ]} + /> ); } ); @@ -387,13 +364,15 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { /> ) : null} {agentToReassign && ( - { - setAgentToReassignId(undefined); - agentsRequest.sendRequest(); - }} - /> + + { + setAgentToReassignId(undefined); + agentsRequest.sendRequest(); + }} + /> + )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 085bad2d18375..6dbc8d67caaee 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -20,5 +20,6 @@ export { appRoutesService, packageToConfigDatasourceInputs, storedDatasourceToAgentDatasource, + configToYaml, AgentStatusKueryHelper, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 0a26a16d35cfd..05a97fd2e2a3c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -26,6 +26,7 @@ export { GetAgentConfigsResponse, GetAgentConfigsResponseItem, GetOneAgentConfigResponse, + GetFullAgentConfigResponse, CreateAgentConfigRequest, CreateAgentConfigResponse, UpdateAgentConfigRequest, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index f74f898b2baf9..afc146cf90447 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; +import { RequestHandler, ResponseHeaders } from 'src/core/server'; import bluebird from 'bluebird'; +import { configToYaml } from '../../../common/services'; import { appContextService, agentConfigService, datasourceService } from '../../services'; import { listAgents } from '../../services/agents'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -229,3 +230,37 @@ export const getFullAgentConfig: RequestHandler> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + const { + params: { agentConfigId }, + } = request; + + try { + const fullAgentConfig = await agentConfigService.getFullConfig(soClient, agentConfigId); + if (fullAgentConfig) { + const body = configToYaml(fullAgentConfig); + const headers: ResponseHeaders = { + 'content-type': 'text/x-yaml', + 'content-disposition': `attachment; filename="elastic-agent-config-${fullAgentConfig.id}.yml"`, + }; + return response.ok({ + body, + headers, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent config not found' }, + }); + } + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts index e630f3c959590..4f6cfb436b93b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts @@ -20,6 +20,7 @@ import { updateAgentConfigHandler, deleteAgentConfigsHandler, getFullAgentConfig, + downloadFullAgentConfig, } from './handlers'; export const registerRoutes = (router: IRouter) => { @@ -82,4 +83,14 @@ export const registerRoutes = (router: IRouter) => { }, getFullAgentConfig ); + + // Download one full agent config + router.get( + { + path: AGENT_CONFIG_API_ROUTES.FULL_INFO_DOWNLOAD_PATTERN, + validate: GetFullAgentConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + downloadFullAgentConfig + ); }; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 483661b9de915..1a0fb262eeb7f 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { AgentStatus } from '../../common/types/models'; +import { AgentStatus } from '../types'; import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts index ab97ddc0ba723..123a413bb8442 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts @@ -39,4 +39,7 @@ export const GetFullAgentConfigRequestSchema = { params: schema.object({ agentConfigId: schema.string(), }), + query: schema.object({ + download: schema.maybe(schema.boolean()), + }), }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0557663e99696..18186f9a1bfcd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8121,11 +8121,9 @@ "xpack.ingestManager.agentConfigForm.systemMonitoringText": "システムメトリックを収集", "xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText": "このオプションを有効にすると、システムメトリックと情報を収集するデータソースで構成をブートストラップできます。", "xpack.ingestManager.agentConfigList.actionsColumnTitle": "アクション", - "xpack.ingestManager.agentConfigList.actionsMenuText": "開く", "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...", @@ -8137,7 +8135,6 @@ "xpack.ingestManager.agentConfigList.reloadAgentConfigsButtonText": "再読み込み", "xpack.ingestManager.agentConfigList.revisionNumber": "rev. {revNumber}", "xpack.ingestManager.agentConfigList.updatedOnColumnTitle": "最終更新日:", - "xpack.ingestManager.agentConfigList.viewConfigActionText": "構成を表示", "xpack.ingestManager.agentDetails.actionsButton": "アクション", "xpack.ingestManager.agentDetails.agentConfigurationLabel": "エージェント構成", "xpack.ingestManager.agentDetails.agentDetailsTitle": "エージェント'{id}'", @@ -8198,7 +8195,6 @@ "xpack.ingestManager.agentHealth.onlineStatusText": "オンライン", "xpack.ingestManager.agentHealth.warningStatusText": "エラー", "xpack.ingestManager.agentList.actionsColumnTitle": "アクション", - "xpack.ingestManager.agentList.actionsMenuText": "開く", "xpack.ingestManager.agentList.addButton": "新しいエージェントを登録", "xpack.ingestManager.agentList.clearFiltersLinkText": "フィルターを消去", "xpack.ingestManager.agentList.configColumnTitle": "構成", @@ -8266,7 +8262,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", "xpack.ingestManager.configDetails.subTabs.datasourcesTabText": "データソース", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "設定", - "xpack.ingestManager.configDetails.subTabs.yamlTabText": "YAML", "xpack.ingestManager.configDetails.summary.datasources": "データソース", "xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:", "xpack.ingestManager.configDetails.summary.revision": "リビジョン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4f62da260d43f..195309f640a0a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8125,11 +8125,9 @@ "xpack.ingestManager.agentConfigForm.systemMonitoringText": "收集系统指标", "xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText": "启用此选项可使用收集系统指标和信息的数据源启动您的配置。", "xpack.ingestManager.agentConfigList.actionsColumnTitle": "操作", - "xpack.ingestManager.agentConfigList.actionsMenuText": "打开", "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……", @@ -8141,7 +8139,6 @@ "xpack.ingestManager.agentConfigList.reloadAgentConfigsButtonText": "重新加载", "xpack.ingestManager.agentConfigList.revisionNumber": "修订 {revNumber}", "xpack.ingestManager.agentConfigList.updatedOnColumnTitle": "最后更新时间", - "xpack.ingestManager.agentConfigList.viewConfigActionText": "查看配置", "xpack.ingestManager.agentDetails.actionsButton": "操作", "xpack.ingestManager.agentDetails.agentConfigurationLabel": "代理配置", "xpack.ingestManager.agentDetails.agentDetailsTitle": "代理“{id}”", @@ -8202,7 +8199,6 @@ "xpack.ingestManager.agentHealth.onlineStatusText": "联机", "xpack.ingestManager.agentHealth.warningStatusText": "错误", "xpack.ingestManager.agentList.actionsColumnTitle": "操作", - "xpack.ingestManager.agentList.actionsMenuText": "打开", "xpack.ingestManager.agentList.addButton": "注册新代理", "xpack.ingestManager.agentList.clearFiltersLinkText": "清除筛选", "xpack.ingestManager.agentList.configColumnTitle": "配置", @@ -8270,7 +8266,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", "xpack.ingestManager.configDetails.subTabs.datasourcesTabText": "数据源", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "设置", - "xpack.ingestManager.configDetails.subTabs.yamlTabText": "YAML", "xpack.ingestManager.configDetails.summary.datasources": "数据源", "xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间", "xpack.ingestManager.configDetails.summary.revision": "修订", From 1d86a2abd2ff4e5e726409c4f7bfb9a31f7ed0d3 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 2 Jun 2020 19:23:15 -0600 Subject: [PATCH 61/69] [SIEM][Exceptions] Add success/error toast component on alert state change (#67406) ## Summary Adds a success and error toast on open or close of alert(s). #65947 Screen Shot 2020-05-26 at 2 28 57 PM Screen Shot 2020-05-28 at 12 01 03 PM ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../alerts/components/signals/actions.tsx | 10 ++++-- .../signals/default_config.test.tsx | 15 ++++++++ .../components/signals/default_config.tsx | 6 ++++ .../alerts/components/signals/index.tsx | 35 +++++++++++++++++++ .../alerts/components/signals/translations.ts | 28 +++++++++++++++ .../public/alerts/components/signals/types.ts | 2 ++ .../detection_engine/signals/api.ts | 3 +- 7 files changed, 95 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx b/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx index c13e064bd1c3c..f01b39ccaba0d 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx @@ -56,17 +56,21 @@ export const updateSignalStatusAction = async ({ status, setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }: UpdateSignalStatusActionProps) => { try { setEventsLoading({ eventIds: signalIds, isLoading: true }); const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds); - await updateSignalStatus({ query: queryObject, status }); + const response = await updateSignalStatus({ query: queryObject, status }); // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds: signalIds, isDeleted: true }); - } catch (e) { - // TODO: Show error toasts + + onAlertStatusUpdateSuccess(response.updated, status); + } catch (error) { + onAlertStatusUpdateFailure(status, error); } finally { setEventsLoading({ eventIds: signalIds, isLoading: false }); } diff --git a/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx index 71da68108da7e..7821bfaaf9575 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx @@ -54,11 +54,16 @@ describe('signals default_config', () => { let createTimeline: CreateTimeline; let updateTimelineIsLoading: UpdateTimelineLoading; + let onAlertStatusUpdateSuccess: (count: number, status: string) => void; + let onAlertStatusUpdateFailure: (status: string, error: Error) => void; + beforeEach(() => { setEventsLoading = jest.fn(); setEventsDeleted = jest.fn(); createTimeline = jest.fn(); updateTimelineIsLoading = jest.fn(); + onAlertStatusUpdateSuccess = jest.fn(); + onAlertStatusUpdateFailure = jest.fn(); }); describe('timeline tooltip', () => { @@ -71,6 +76,8 @@ describe('signals default_config', () => { createTimeline, status: 'open', updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); const timelineAction = signalsActions[0].getAction({ eventId: 'even-id', @@ -97,6 +104,8 @@ describe('signals default_config', () => { createTimeline, status: 'open', updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); signalOpenAction = signalsActions[1].getAction({ @@ -119,6 +128,8 @@ describe('signals default_config', () => { status: 'open', setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); }); @@ -151,6 +162,8 @@ describe('signals default_config', () => { createTimeline, status: 'closed', updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); signalCloseAction = signalsActions[1].getAction({ @@ -173,6 +186,8 @@ describe('signals default_config', () => { status: 'closed', setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx b/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx index 05e0baba66d0a..1269f31064e9e 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx @@ -198,6 +198,8 @@ export const getSignalsActions = ({ createTimeline, status, updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }: { apolloClient?: ApolloClient<{}>; canUserCRUD: boolean; @@ -207,6 +209,8 @@ export const getSignalsActions = ({ createTimeline: CreateTimeline; status: 'open' | 'closed'; updateTimelineIsLoading: UpdateTimelineLoading; + onAlertStatusUpdateSuccess: (count: number, status: string) => void; + onAlertStatusUpdateFailure: (status: string, error: Error) => void; }): TimelineAction[] => [ { getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( @@ -246,6 +250,8 @@ export const getSignalsActions = ({ status, setEventsLoading, setEventsDeleted, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }) } isDisabled={!canUserCRUD || !hasIndexWrite} diff --git a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals/index.tsx index eb19cfea97324..effb6a2450dc2 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/index.tsx @@ -46,6 +46,11 @@ import { UpdateSignalsStatusProps, } from './types'; import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; +import { + useStateToaster, + displaySuccessToast, + displayErrorToast, +} from '../../../common/components/toasters'; export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; @@ -91,6 +96,7 @@ export const SignalsTableComponent: React.FC = ({ signalsIndex !== '' ? [signalsIndex] : [] ); const kibana = useKibana(); + const [, dispatchToaster] = useStateToaster(); const getGlobalQuery = useCallback(() => { if (browserFields != null && indexPatterns != null) { @@ -146,6 +152,27 @@ export const SignalsTableComponent: React.FC = ({ [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] ); + const onAlertStatusUpdateSuccess = useCallback( + (count: number, status: string) => { + const title = + status === 'closed' + ? i18n.CLOSED_ALERT_SUCCESS_TOAST(count) + : i18n.OPENED_ALERT_SUCCESS_TOAST(count); + + displaySuccessToast(title, dispatchToaster); + }, + [dispatchToaster] + ); + + const onAlertStatusUpdateFailure = useCallback( + (status: string, error: Error) => { + const title = + status === 'closed' ? i18n.CLOSED_ALERT_FAILED_TOAST : i18n.OPENED_ALERT_FAILED_TOAST; + displayErrorToast(title, [error.message], dispatchToaster); + }, + [dispatchToaster] + ); + // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (!isSelectAllChecked) { @@ -189,6 +216,8 @@ export const SignalsTableComponent: React.FC = ({ status, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }); refetchQuery(); }, @@ -198,6 +227,8 @@ export const SignalsTableComponent: React.FC = ({ setEventsDeletedCallback, setEventsLoadingCallback, showClearSelectionAction, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, ] ); @@ -244,6 +275,8 @@ export const SignalsTableComponent: React.FC = ({ setEventsDeleted: setEventsDeletedCallback, status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, }), [ apolloClient, @@ -254,6 +287,8 @@ export const SignalsTableComponent: React.FC = ({ setEventsLoadingCallback, setEventsDeletedCallback, updateTimelineIsLoading, + onAlertStatusUpdateSuccess, + onAlertStatusUpdateFailure, ] ); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals/translations.ts index f68dcd932bc32..e49ff9846b9b7 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/translations.ts +++ b/x-pack/plugins/siem/public/alerts/components/signals/translations.ts @@ -101,3 +101,31 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( defaultMessage: 'Investigate in timeline', } ); + +export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.signals.closedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.signals.openedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const CLOSED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.siem.detectionEngine.signals.closedAlertFailedToastMessage', + { + defaultMessage: 'Failed to close alert(s).', + } +); + +export const OPENED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.siem.detectionEngine.signals.openedAlertFailedToastMessage', + { + defaultMessage: 'Failed to open alert(s)', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/types.ts b/x-pack/plugins/siem/public/alerts/components/signals/types.ts index b3c770415ed57..542aa61074ce1 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/types.ts +++ b/x-pack/plugins/siem/public/alerts/components/signals/types.ts @@ -37,6 +37,8 @@ export interface UpdateSignalStatusActionProps { status: 'open' | 'closed'; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; + onAlertStatusUpdateSuccess: (count: number, status: string) => void; + onAlertStatusUpdateFailure: (status: string, error: Error) => void; } export type SendSignalsToTimeline = () => void; diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts index 860305dd58e67..445f66457c59e 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReindexResponse } from 'elasticsearch'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -54,7 +55,7 @@ export const updateSignalStatus = async ({ query, status, signal, -}: UpdateSignalStatusProps): Promise => +}: UpdateSignalStatusProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, ...query }), From d8b66cf7d05b0ae6cc896498c98d70c8176bceb2 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 2 Jun 2020 21:23:37 -0400 Subject: [PATCH 62/69] [Uptime] Switch from `EuiFieldNumber` to `EuiFieldText` on settings page (#66425) * Switch from `EuiFieldNumber` to `EuiFieldText` on settings page. * Add unit tests for cert form fields. * Improve validation. * Add additional server-side settings check with tests. * Only create number object for validation when value is string. * Clean up validation function. Co-authored-by: Elastic Machine --- x-pack/plugins/uptime/common/translations.ts | 6 +- .../__tests__/certificate_form.test.tsx | 108 +++++++++++++++++- .../components/settings/certificate_form.tsx | 16 +-- .../public/pages/__tests__/settings.test.tsx | 35 ++++++ .../plugins/uptime/public/pages/settings.tsx | 18 ++- .../uptime/public/pages/translations.ts | 3 + .../__tests__/dynamic_settings.test.ts | 77 +++++++++++++ .../server/rest_api/dynamic_settings.ts | 17 ++- 8 files changed, 260 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/uptime/public/pages/__tests__/settings.test.tsx create mode 100644 x-pack/plugins/uptime/server/rest_api/__tests__/dynamic_settings.test.ts diff --git a/x-pack/plugins/uptime/common/translations.ts b/x-pack/plugins/uptime/common/translations.ts index 678fe7cb1f984..81f46df86f02e 100644 --- a/x-pack/plugins/uptime/common/translations.ts +++ b/x-pack/plugins/uptime/common/translations.ts @@ -6,9 +6,13 @@ import { i18n } from '@kbn/i18n'; -export const VALUE_MUST_BE_GREATER_THEN_ZEO = i18n.translate( +export const VALUE_MUST_BE_GREATER_THAN_ZERO = i18n.translate( 'xpack.uptime.settings.invalid.error', { defaultMessage: 'Value must be greater than 0.', } ); + +export const VALUE_MUST_BE_AN_INTEGER = i18n.translate('xpack.uptime.settings.invalid.nanError', { + defaultMessage: 'Value must be an integer.', +}); diff --git a/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx index 8e2348a60ddab..2b587d23247e2 100644 --- a/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx +++ b/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { CertificateExpirationForm } from '../certificate_form'; -import { shallowWithRouter } from '../../../lib'; +import { shallowWithRouter, mountWithRouter } from '../../../lib'; describe('CertificateForm', () => { it('shallow renders expected elements for valid props', () => { @@ -26,4 +26,110 @@ describe('CertificateForm', () => { ) ).toMatchSnapshot(); }); + + it('submits number values for certs settings fields', () => { + const onChangeMock = jest.fn(); + const wrapper = mountWithRouter( + + ); + + const inputs = wrapper.find('input'); + + expect(inputs).toHaveLength(2); + + // expiration threshold input + inputs.at(0).simulate('change', { + target: { + value: '23', + }, + }); + + // age threshold input + inputs.at(1).simulate('change', { + target: { + value: '56', + }, + }); + + expect(onChangeMock).toHaveBeenCalledTimes(2); + + expect(onChangeMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "certExpirationThreshold": 23, + }, + ] + `); + + expect(onChangeMock.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "certAgeThreshold": 56, + }, + ] + `); + }); + + it('submits undefined for NaN values', () => { + const onChangeMock = jest.fn(); + const wrapper = mountWithRouter( + + ); + + const inputs = wrapper.find('input'); + + expect(inputs).toHaveLength(2); + + // expiration threshold input + inputs.at(0).simulate('change', { + target: { + value: 'A', + }, + }); + + // age threshold input + inputs.at(1).simulate('change', { + target: { + value: 'g', + }, + }); + + expect(onChangeMock).toHaveBeenCalledTimes(2); + + expect(onChangeMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "certExpirationThreshold": undefined, + }, + ] + `); + + expect(onChangeMock.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "certAgeThreshold": undefined, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx index 38c0d0663c937..dcc3b97575983 100644 --- a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx +++ b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx @@ -10,7 +10,7 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiCode, - EuiFieldNumber, + EuiFieldText, EuiText, EuiTitle, EuiSpacer, @@ -80,17 +80,17 @@ export const CertificateExpirationForm: React.FC = ({ > - onChange({ - certExpirationThreshold: Number(e.target.value), + certExpirationThreshold: Number(e.target.value) || undefined, }) } /> @@ -128,17 +128,17 @@ export const CertificateExpirationForm: React.FC = ({ > - + onChange={({ target: { value } }) => onChange({ - certAgeThreshold: Number(value), + certAgeThreshold: Number(value) || undefined, }) } /> diff --git a/x-pack/plugins/uptime/public/pages/__tests__/settings.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/settings.test.tsx new file mode 100644 index 0000000000000..601feee8f8159 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/__tests__/settings.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isValidCertVal } from '../settings'; + +describe('settings', () => { + describe('isValidCertVal', () => { + it('handles NaN values', () => { + expect(isValidCertVal(NaN)).toMatchInlineSnapshot(`"Must be a number."`); + }); + + it('handles undefined', () => { + expect(isValidCertVal(undefined)).toMatchInlineSnapshot(`"Must be a number."`); + }); + + it('handles non-integer numbers', () => { + expect(isValidCertVal(23.5)).toMatchInlineSnapshot(`"Value must be an integer."`); + }); + + it('handles values less than 0', () => { + expect(isValidCertVal(-1)).toMatchInlineSnapshot(`"Value must be greater than 0."`); + }); + + it('handles 0', () => { + expect(isValidCertVal(0)).toMatchInlineSnapshot(`"Value must be greater than 0."`); + }); + + it('allows valid integer numbers', () => { + expect(isValidCertVal(67)).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index 807da83821261..6285c0cc0f1da 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -31,7 +31,10 @@ import { OnFieldChangeType, } from '../components/settings/certificate_form'; import * as Translations from './translations'; -import { VALUE_MUST_BE_GREATER_THEN_ZEO } from '../../common/translations'; +import { + VALUE_MUST_BE_GREATER_THAN_ZERO, + VALUE_MUST_BE_AN_INTEGER, +} from '../../common/translations'; interface SettingsPageFieldErrors { heartbeatIndices: string | ''; @@ -47,12 +50,15 @@ export interface SettingsFormProps { isDisabled: boolean; } -const isValidCertVal = (val: string | number) => { - if (val === '') { - return Translations.BLANK_STR; +export const isValidCertVal = (val?: number): string | undefined => { + if (val === undefined || isNaN(val)) { + return Translations.settings.mustBeNumber; + } + if (val <= 0) { + return VALUE_MUST_BE_GREATER_THAN_ZERO; } - if (val === 0) { - return VALUE_MUST_BE_GREATER_THEN_ZEO; + if (val % 1) { + return VALUE_MUST_BE_AN_INTEGER; } }; diff --git a/x-pack/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts index 8ed5503235884..dae4760edc469 100644 --- a/x-pack/plugins/uptime/public/pages/translations.ts +++ b/x-pack/plugins/uptime/public/pages/translations.ts @@ -35,6 +35,9 @@ export const settings = { returnToOverviewLinkLabel: i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { defaultMessage: 'Return to overview', }), + mustBeNumber: i18n.translate('xpack.uptime.settings.blankNumberField.error', { + defaultMessage: 'Must be a number.', + }), }; export const BLANK_STR = i18n.translate('xpack.uptime.settings.blank.error', { diff --git a/x-pack/plugins/uptime/server/rest_api/__tests__/dynamic_settings.test.ts b/x-pack/plugins/uptime/server/rest_api/__tests__/dynamic_settings.test.ts new file mode 100644 index 0000000000000..32e6385bc9fa2 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/__tests__/dynamic_settings.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateCertsValues } from '../dynamic_settings'; + +describe('dynamic settings', () => { + describe('validateCertValues', () => { + it(`doesn't allow age threshold values less than 0`, () => { + expect( + validateCertsValues({ + certAgeThreshold: -1, + certExpirationThreshold: 2, + heartbeatIndices: 'foo', + }) + ).toMatchInlineSnapshot(` + Object { + "certAgeThreshold": "Value must be greater than 0.", + } + `); + }); + + it(`doesn't allow non-integer age threshold values`, () => { + expect( + validateCertsValues({ + certAgeThreshold: 10.2, + certExpirationThreshold: 2, + heartbeatIndices: 'foo', + }) + ).toMatchInlineSnapshot(` + Object { + "certAgeThreshold": "Value must be an integer.", + } + `); + }); + + it(`doesn't allow expiration threshold values less than 0`, () => { + expect( + validateCertsValues({ + certAgeThreshold: 2, + certExpirationThreshold: -1, + heartbeatIndices: 'foo', + }) + ).toMatchInlineSnapshot(` + Object { + "certExpirationThreshold": "Value must be greater than 0.", + } + `); + }); + + it(`doesn't allow non-integer expiration threshold values`, () => { + expect( + validateCertsValues({ + certAgeThreshold: 2, + certExpirationThreshold: 1.23, + heartbeatIndices: 'foo', + }) + ).toMatchInlineSnapshot(` + Object { + "certExpirationThreshold": "Value must be an integer.", + } + `); + }); + + it('allows valid values', () => { + expect( + validateCertsValues({ + certAgeThreshold: 2, + certExpirationThreshold: 13, + heartbeatIndices: 'foo', + }) + ).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index c7d532d932aa6..6dba36ec0613d 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -11,7 +11,10 @@ import { UMServerLibs } from '../lib/lib'; import { DynamicSettings, DynamicSettingsType } from '../../common/runtime_types'; import { UMRestApiRouteFactory } from '.'; import { savedObjectsAdapter } from '../lib/saved_objects'; -import { VALUE_MUST_BE_GREATER_THEN_ZEO } from '../../common/translations'; +import { + VALUE_MUST_BE_GREATER_THAN_ZERO, + VALUE_MUST_BE_AN_INTEGER, +} from '../../common/translations'; export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -24,13 +27,19 @@ export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSer }, }); -const validateCertsValues = (settings: DynamicSettings) => { +export const validateCertsValues = ( + settings: DynamicSettings +): Record | undefined => { const errors: any = {}; if (settings.certAgeThreshold <= 0) { - errors.certAgeThreshold = VALUE_MUST_BE_GREATER_THEN_ZEO; + errors.certAgeThreshold = VALUE_MUST_BE_GREATER_THAN_ZERO; + } else if (settings.certAgeThreshold % 1) { + errors.certAgeThreshold = VALUE_MUST_BE_AN_INTEGER; } if (settings.certExpirationThreshold <= 0) { - errors.certExpirationThreshold = VALUE_MUST_BE_GREATER_THEN_ZEO; + errors.certExpirationThreshold = VALUE_MUST_BE_GREATER_THAN_ZERO; + } else if (settings.certExpirationThreshold % 1) { + errors.certExpirationThreshold = VALUE_MUST_BE_AN_INTEGER; } if (errors.certAgeThreshold || errors.certExpirationThreshold) { return errors; From fdd0c47e75f80b34e3c2e50b6ce6186cb8bc0abe Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 2 Jun 2020 19:42:03 -0700 Subject: [PATCH 63/69] skip flaky suite (#53749) --- x-pack/test/functional/apps/graph/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index d9e8b22c45d50..803e5e8f80d70 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('graph', function () { + // FLAKY: https://github.com/elastic/kibana/issues/53749 + describe.skip('graph', function () { before(async () => { await browser.setWindowSize(1600, 1000); log.debug('load graph/secrepo data'); From 4c30d912a9c08079c8caf96f73a16f531c125a5f Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 2 Jun 2020 21:50:16 -0600 Subject: [PATCH 64/69] [SIEM] [Detections] Renames Signals to Alerts (#67731) ## Summary Resolves https://github.com/elastic/kibana/issues/65944 Renames `Signals` -> `Alerts` on the main Detection Engine page. Including: * Timeline Event Selector * Alerts Histogram * Alerts Table Does not include: * `Detections` -> `Alerts` navigation rename * `SignalsByCategory` rename as there already exists an `AlertsByCategory`, verify changing to `ExternalAlertsByCategory` * Anything server-side or related to `siemSignalsIndex` ### Checklist - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [X] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- x-pack/plugins/siem/cypress/README.md | 8 +- .../cypress/integration/detections.spec.ts | 231 +++++++++--------- .../integration/detections_timeline.spec.ts | 22 +- .../signal_detection_rules.spec.ts | 18 +- .../signal_detection_rules_custom.spec.ts | 24 +- .../signal_detection_rules_export.spec.ts | 14 +- .../signal_detection_rules_ml.spec.ts | 18 +- .../signal_detection_rules_prebuilt.spec.ts | 24 +- ...tion_rules.ts => alert_detection_rules.ts} | 0 .../siem/cypress/screens/detections.ts | 28 +-- ...tion_rules.ts => alert_detection_rules.ts} | 2 +- .../plugins/siem/cypress/tasks/detections.ts | 78 +++--- .../alerts_histogram.test.tsx} | 6 +- .../alerts_histogram.tsx} | 14 +- .../config.ts | 4 +- .../helpers.test.tsx | 8 +- .../helpers.tsx | 34 ++- .../index.test.tsx | 6 +- .../index.tsx | 106 ++++---- .../translations.ts | 48 ++-- .../types.ts | 18 +- .../alerts/components/alerts_info/index.tsx | 49 ++++ .../query.dsl.ts | 2 +- .../{signals_info => alerts_info}/types.ts | 0 .../actions.test.tsx | 44 ++-- .../{signals => alerts_table}/actions.tsx | 38 +-- .../alerts_filter_group}/index.test.tsx | 6 +- .../alerts_filter_group}/index.tsx | 16 +- .../alerts_utility_bar}/index.test.tsx | 10 +- .../alerts_utility_bar}/index.tsx | 36 +-- .../alerts_utility_bar/translations.ts | 77 ++++++ .../default_config.test.tsx | 80 +++--- .../default_config.tsx | 50 ++-- .../{signals => alerts_table}/helpers.test.ts | 0 .../{signals => alerts_table}/helpers.ts | 2 +- .../{signals => alerts_table}/index.test.tsx | 8 +- .../{signals => alerts_table}/index.tsx | 94 +++---- .../components/alerts_table/translations.ts | 128 ++++++++++ .../{signals => alerts_table}/types.ts | 18 +- .../translations.ts | 2 +- .../index.test.tsx | 6 +- .../index.tsx | 8 +- .../translations.ts | 14 +- .../rules/pre_packaged_rules/translations.ts | 2 +- .../rules/rule_actions_field/index.tsx | 2 +- .../rules/step_about_rule/schema.tsx | 2 +- .../rules/step_define_rule/schema.tsx | 2 +- .../rules/step_schedule_rule/schema.tsx | 5 +- .../signals_utility_bar/translations.ts | 77 ------ .../alerts/components/signals/translations.ts | 131 ---------- .../alerts/components/signals_info/index.tsx | 49 ---- .../components/user_info/index.test.tsx | 8 +- .../alerts/components/user_info/index.tsx | 4 +- .../{signals => alerts}/__mocks__/api.ts | 20 +- .../{signals => alerts}/api.test.ts | 58 ++--- .../{signals => alerts}/api.ts | 34 +-- .../{signals => alerts}/mock.ts | 18 +- .../{signals => alerts}/translations.ts | 14 +- .../{signals => alerts}/types.ts | 12 +- .../use_privilege_user.test.tsx | 0 .../use_privilege_user.tsx | 0 .../{signals => alerts}/use_query.test.tsx | 62 ++--- .../{signals => alerts}/use_query.tsx | 30 +-- .../use_signal_index.test.tsx | 4 +- .../{signals => alerts}/use_signal_index.tsx | 0 .../detection_engine/detection_engine.tsx | 116 +++------ .../alerts/pages/detection_engine/index.tsx | 9 +- .../rules/create/helpers.test.ts | 2 +- .../rules/create/translations.ts | 2 +- .../detection_engine/rules/details/index.tsx | 52 ++-- .../rules/details/translations.ts | 2 +- .../pages/detection_engine/rules/index.tsx | 2 +- .../detection_engine/rules/translations.ts | 11 +- .../detection_engine/rules/utils.test.ts | 2 +- .../pages/detection_engine/rules/utils.ts | 7 - .../pages/detection_engine/translations.ts | 16 +- .../alerts/pages/detection_engine/types.ts | 10 - .../components/drag_and_drop/helpers.ts | 2 +- .../common/components/link_to/link_to.tsx | 6 - .../link_to/redirect_to_detection_engine.tsx | 11 +- .../popover_description.test.tsx.snap | 2 +- .../ml_popover/popover_description.tsx | 2 +- .../common/components/top_n/helpers.test.tsx | 6 +- .../public/common/components/top_n/helpers.ts | 16 +- .../common/components/top_n/index.test.tsx | 6 +- .../public/common/components/top_n/index.tsx | 10 +- .../common/components/top_n/top_n.test.tsx | 10 +- .../common/components/top_n/translations.ts | 4 +- .../siem/public/common/mock/mock_ecs.ts | 2 +- .../public/common/mock/timeline_results.ts | 4 +- .../components/alerts_by_category/index.tsx | 11 +- .../components/signals_by_category/index.tsx | 18 +- .../public/overview/pages/translations.ts | 4 +- .../components/fields_browser/header.tsx | 10 +- .../timelines/components/timeline/index.tsx | 2 +- .../timeline/search_or_filter/pick_events.tsx | 2 +- .../public/timelines/containers/index.tsx | 4 +- .../public/timelines/store/timeline/model.ts | 2 +- .../routes/__mocks__/request_responses.ts | 2 +- .../lib/matrix_histogram/translations.ts | 2 +- .../translations/translations/ja-JP.json | 51 ---- .../translations/translations/zh-CN.json | 51 ---- .../{signals => alerts}/data.json.gz | Bin .../{signals => alerts}/mappings.json | 0 .../data.json.gz | Bin .../mappings.json | 0 .../data.json.gz | Bin .../mappings.json | 0 108 files changed, 1099 insertions(+), 1305 deletions(-) rename x-pack/plugins/siem/cypress/screens/{signal_detection_rules.ts => alert_detection_rules.ts} (100%) rename x-pack/plugins/siem/cypress/tasks/{signal_detection_rules.ts => alert_detection_rules.ts} (98%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel/signals_histogram.test.tsx => alerts_histogram_panel/alerts_histogram.test.tsx} (84%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel/signals_histogram.tsx => alerts_histogram_panel/alerts_histogram.tsx} (89%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/config.ts (88%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/helpers.test.tsx (89%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/helpers.tsx (67%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/index.test.tsx (85%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/index.tsx (72%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/translations.ts (50%) rename x-pack/plugins/siem/public/alerts/components/{signals_histogram_panel => alerts_histogram_panel}/types.ts (70%) create mode 100644 x-pack/plugins/siem/public/alerts/components/alerts_info/index.tsx rename x-pack/plugins/siem/public/alerts/components/{signals_info => alerts_info}/query.dsl.ts (91%) rename x-pack/plugins/siem/public/alerts/components/{signals_info => alerts_info}/types.ts (100%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/actions.test.tsx (92%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/actions.tsx (82%) rename x-pack/plugins/siem/public/alerts/components/{signals/signals_filter_group => alerts_table/alerts_filter_group}/index.test.tsx (68%) rename x-pack/plugins/siem/public/alerts/components/{signals/signals_filter_group => alerts_table/alerts_filter_group}/index.tsx (74%) rename x-pack/plugins/siem/public/alerts/components/{signals/signals_utility_bar => alerts_table/alerts_utility_bar}/index.test.tsx (76%) rename x-pack/plugins/siem/public/alerts/components/{signals/signals_utility_bar => alerts_table/alerts_utility_bar}/index.tsx (77%) create mode 100644 x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/default_config.test.tsx (67%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/default_config.tsx (81%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/helpers.test.ts (100%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/helpers.ts (98%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/index.test.tsx (85%) rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/index.tsx (81%) create mode 100644 x-pack/plugins/siem/public/alerts/components/alerts_table/translations.ts rename x-pack/plugins/siem/public/alerts/components/{signals => alerts_table}/types.ts (77%) rename x-pack/plugins/siem/public/alerts/components/{no_write_signals_callout => no_write_alerts_callout}/index.test.tsx (72%) rename x-pack/plugins/siem/public/alerts/components/{no_write_signals_callout => no_write_alerts_callout}/index.tsx (72%) rename x-pack/plugins/siem/public/alerts/components/{no_write_signals_callout => no_write_alerts_callout}/translations.ts (52%) delete mode 100644 x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/translations.ts delete mode 100644 x-pack/plugins/siem/public/alerts/components/signals/translations.ts delete mode 100644 x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/__mocks__/api.ts (56%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/api.test.ts (68%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/api.ts (72%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/mock.ts (98%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/translations.ts (55%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/types.ts (88%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/use_privilege_user.test.tsx (100%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/use_privilege_user.tsx (100%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/use_query.test.tsx (61%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/use_query.tsx (69%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/use_signal_index.test.tsx (96%) rename x-pack/plugins/siem/public/alerts/containers/detection_engine/{signals => alerts}/use_signal_index.tsx (100%) delete mode 100644 x-pack/plugins/siem/public/alerts/pages/detection_engine/types.ts rename x-pack/test/siem_cypress/es_archives/{signals => alerts}/data.json.gz (100%) rename x-pack/test/siem_cypress/es_archives/{signals => alerts}/mappings.json (100%) rename x-pack/test/siem_cypress/es_archives/{closed_signals => closed_alerts}/data.json.gz (100%) rename x-pack/test/siem_cypress/es_archives/{closed_signals => closed_alerts}/mappings.json (100%) rename x-pack/test/siem_cypress/es_archives/{timeline_signals => timeline_alerts}/data.json.gz (100%) rename x-pack/test/siem_cypress/es_archives/{timeline_signals => timeline_alerts}/mappings.json (100%) diff --git a/x-pack/plugins/siem/cypress/README.md b/x-pack/plugins/siem/cypress/README.md index d84c66fec1c3a..b50924532726c 100644 --- a/x-pack/plugins/siem/cypress/README.md +++ b/x-pack/plugins/siem/cypress/README.md @@ -176,16 +176,16 @@ The current archives can be found in `x-pack/test/siem_cypress/es_archives/`. - siem-kibana - siem-es - jessie -- closed_signals - - Set of data with 108 closed signals linked to "Signals test" custom rule. +- closed_alerts + - Set of data with 108 closed alerts linked to "Alerts test" custom rule. - custome_rules - Set if data with just 4 custom activated rules. - empty_kibana - Empty kibana board. - prebuilt_rules_loaded - Elastic prebuilt loaded rules and deactivated. -- signals - - Set of data with 108 opened signals linked to "Signals test" custom rule. +- alerts + - Set of data with 108 opened alerts linked to "Alerts test" custom rule. ### How to generate a new archive diff --git a/x-pack/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/plugins/siem/cypress/integration/detections.spec.ts index 91727595708f6..23e84070e93ae 100644 --- a/x-pack/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/detections.spec.ts @@ -4,24 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ import { - NUMBER_OF_SIGNALS, - OPEN_CLOSE_SIGNALS_BTN, - SELECTED_SIGNALS, - SHOWING_SIGNALS, - SIGNALS, + NUMBER_OF_ALERTS, + OPEN_CLOSE_ALERTS_BTN, + SELECTED_ALERTS, + SHOWING_ALERTS, + ALERTS, } from '../screens/detections'; import { - closeFirstSignal, - closeSignals, - goToClosedSignals, - goToOpenedSignals, - openFirstSignal, - openSignals, - selectNumberOfSignals, - waitForSignalsPanelToBeLoaded, - waitForSignals, - waitForSignalsToBeLoaded, + closeFirstAlert, + closeAlerts, + goToClosedAlerts, + goToOpenedAlerts, + openFirstAlert, + openAlerts, + selectNumberOfAlerts, + waitForAlertsPanelToBeLoaded, + waitForAlerts, + waitForAlertsToBeLoaded, } from '../tasks/detections'; import { esArchiverLoad } from '../tasks/es_archiver'; import { loginAndWaitForPage } from '../tasks/login'; @@ -29,179 +29,176 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('Detections', () => { - context('Closing signals', () => { + context('Closing alerts', () => { beforeEach(() => { - esArchiverLoad('signals'); + esArchiverLoad('alerts'); loginAndWaitForPage(DETECTIONS); }); - it('Closes and opens signals', () => { - waitForSignalsPanelToBeLoaded(); - waitForSignalsToBeLoaded(); + it('Closes and opens alerts', () => { + waitForAlertsPanelToBeLoaded(); + waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_SIGNALS) + cy.get(NUMBER_OF_ALERTS) .invoke('text') - .then((numberOfSignals) => { - cy.get(SHOWING_SIGNALS).should('have.text', `Showing ${numberOfSignals} signals`); + .then((numberOfAlerts) => { + cy.get(SHOWING_ALERTS).should('have.text', `Showing ${numberOfAlerts} alerts`); - const numberOfSignalsToBeClosed = 3; - selectNumberOfSignals(numberOfSignalsToBeClosed); + const numberOfAlertsToBeClosed = 3; + selectNumberOfAlerts(numberOfAlertsToBeClosed); - cy.get(SELECTED_SIGNALS).should( + cy.get(SELECTED_ALERTS).should( 'have.text', - `Selected ${numberOfSignalsToBeClosed} signals` + `Selected ${numberOfAlertsToBeClosed} alerts` ); - closeSignals(); - waitForSignals(); + closeAlerts(); + waitForAlerts(); cy.reload(); - waitForSignals(); + waitForAlerts(); - const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; - cy.get(NUMBER_OF_SIGNALS).should( + const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; + cy.get(NUMBER_OF_ALERTS).should( 'have.text', - expectedNumberOfSignalsAfterClosing.toString() + expectedNumberOfAlertsAfterClosing.toString() ); - cy.get(SHOWING_SIGNALS).should( + cy.get(SHOWING_ALERTS).should( 'have.text', - `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals` + `Showing ${expectedNumberOfAlertsAfterClosing.toString()} alerts` ); - goToClosedSignals(); - waitForSignals(); + goToClosedAlerts(); + waitForAlerts(); - cy.get(NUMBER_OF_SIGNALS).should('have.text', numberOfSignalsToBeClosed.toString()); - cy.get(SHOWING_SIGNALS).should( + cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeClosed.toString()); + cy.get(SHOWING_ALERTS).should( 'have.text', - `Showing ${numberOfSignalsToBeClosed.toString()} signals` + `Showing ${numberOfAlertsToBeClosed.toString()} alerts` ); - cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + cy.get(ALERTS).should('have.length', numberOfAlertsToBeClosed); - const numberOfSignalsToBeOpened = 1; - selectNumberOfSignals(numberOfSignalsToBeOpened); + const numberOfAlertsToBeOpened = 1; + selectNumberOfAlerts(numberOfAlertsToBeOpened); - cy.get(SELECTED_SIGNALS).should( - 'have.text', - `Selected ${numberOfSignalsToBeOpened} signal` - ); + cy.get(SELECTED_ALERTS).should('have.text', `Selected ${numberOfAlertsToBeOpened} alert`); - openSignals(); - waitForSignals(); + openAlerts(); + waitForAlerts(); cy.reload(); - waitForSignalsToBeLoaded(); - waitForSignals(); - goToClosedSignals(); - waitForSignals(); + waitForAlertsToBeLoaded(); + waitForAlerts(); + goToClosedAlerts(); + waitForAlerts(); - const expectedNumberOfClosedSignalsAfterOpened = 2; - cy.get(NUMBER_OF_SIGNALS).should( + const expectedNumberOfClosedAlertsAfterOpened = 2; + cy.get(NUMBER_OF_ALERTS).should( 'have.text', - expectedNumberOfClosedSignalsAfterOpened.toString() + expectedNumberOfClosedAlertsAfterOpened.toString() ); - cy.get(SHOWING_SIGNALS).should( + cy.get(SHOWING_ALERTS).should( 'have.text', - `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals` + `Showing ${expectedNumberOfClosedAlertsAfterOpened.toString()} alerts` ); - cy.get(SIGNALS).should('have.length', expectedNumberOfClosedSignalsAfterOpened); + cy.get(ALERTS).should('have.length', expectedNumberOfClosedAlertsAfterOpened); - goToOpenedSignals(); - waitForSignals(); + goToOpenedAlerts(); + waitForAlerts(); - const expectedNumberOfOpenedSignals = - +numberOfSignals - expectedNumberOfClosedSignalsAfterOpened; - cy.get(SHOWING_SIGNALS).should( + const expectedNumberOfOpenedAlerts = + +numberOfAlerts - expectedNumberOfClosedAlertsAfterOpened; + cy.get(SHOWING_ALERTS).should( 'have.text', - `Showing ${expectedNumberOfOpenedSignals.toString()} signals` + `Showing ${expectedNumberOfOpenedAlerts.toString()} alerts` ); cy.get('[data-test-subj="server-side-event-count"]').should( 'have.text', - expectedNumberOfOpenedSignals.toString() + expectedNumberOfOpenedAlerts.toString() ); }); }); - it('Closes one signal when more than one opened signals are selected', () => { - waitForSignalsToBeLoaded(); + it('Closes one alert when more than one opened alerts are selected', () => { + waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_SIGNALS) + cy.get(NUMBER_OF_ALERTS) .invoke('text') - .then((numberOfSignals) => { - const numberOfSignalsToBeClosed = 1; - const numberOfSignalsToBeSelected = 3; + .then((numberOfAlerts) => { + const numberOfAlertsToBeClosed = 1; + const numberOfAlertsToBeSelected = 3; - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); - selectNumberOfSignals(numberOfSignalsToBeSelected); - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); + cy.get(OPEN_CLOSE_ALERTS_BTN).should('have.attr', 'disabled'); + selectNumberOfAlerts(numberOfAlertsToBeSelected); + cy.get(OPEN_CLOSE_ALERTS_BTN).should('not.have.attr', 'disabled'); - closeFirstSignal(); + closeFirstAlert(); cy.reload(); - waitForSignalsToBeLoaded(); - waitForSignals(); + waitForAlertsToBeLoaded(); + waitForAlerts(); - const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; - cy.get(NUMBER_OF_SIGNALS).invoke('text').should('eq', expectedNumberOfSignals.toString()); - cy.get(SHOWING_SIGNALS) + const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeClosed; + cy.get(NUMBER_OF_ALERTS).invoke('text').should('eq', expectedNumberOfAlerts.toString()); + cy.get(SHOWING_ALERTS) .invoke('text') - .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); + .should('eql', `Showing ${expectedNumberOfAlerts.toString()} alerts`); - goToClosedSignals(); - waitForSignals(); + goToClosedAlerts(); + waitForAlerts(); - cy.get(NUMBER_OF_SIGNALS) + cy.get(NUMBER_OF_ALERTS) .invoke('text') - .should('eql', numberOfSignalsToBeClosed.toString()); - cy.get(SHOWING_SIGNALS) + .should('eql', numberOfAlertsToBeClosed.toString()); + cy.get(SHOWING_ALERTS) .invoke('text') - .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); - cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + .should('eql', `Showing ${numberOfAlertsToBeClosed.toString()} alert`); + cy.get(ALERTS).should('have.length', numberOfAlertsToBeClosed); }); }); }); - context('Opening signals', () => { + context('Opening alerts', () => { beforeEach(() => { - esArchiverLoad('closed_signals'); + esArchiverLoad('closed_alerts'); loginAndWaitForPage(DETECTIONS); }); - it('Open one signal when more than one closed signals are selected', () => { - waitForSignals(); - goToClosedSignals(); - waitForSignalsToBeLoaded(); + it('Open one alert when more than one closed alerts are selected', () => { + waitForAlerts(); + goToClosedAlerts(); + waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_SIGNALS) + cy.get(NUMBER_OF_ALERTS) .invoke('text') - .then((numberOfSignals) => { - const numberOfSignalsToBeOpened = 1; - const numberOfSignalsToBeSelected = 3; + .then((numberOfAlerts) => { + const numberOfAlertsToBeOpened = 1; + const numberOfAlertsToBeSelected = 3; - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); - selectNumberOfSignals(numberOfSignalsToBeSelected); - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); + cy.get(OPEN_CLOSE_ALERTS_BTN).should('have.attr', 'disabled'); + selectNumberOfAlerts(numberOfAlertsToBeSelected); + cy.get(OPEN_CLOSE_ALERTS_BTN).should('not.have.attr', 'disabled'); - openFirstSignal(); + openFirstAlert(); cy.reload(); - goToClosedSignals(); - waitForSignalsToBeLoaded(); - waitForSignals(); + goToClosedAlerts(); + waitForAlertsToBeLoaded(); + waitForAlerts(); - const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeOpened; - cy.get(NUMBER_OF_SIGNALS).invoke('text').should('eq', expectedNumberOfSignals.toString()); - cy.get(SHOWING_SIGNALS) + const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeOpened; + cy.get(NUMBER_OF_ALERTS).invoke('text').should('eq', expectedNumberOfAlerts.toString()); + cy.get(SHOWING_ALERTS) .invoke('text') - .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); + .should('eql', `Showing ${expectedNumberOfAlerts.toString()} alerts`); - goToOpenedSignals(); - waitForSignals(); + goToOpenedAlerts(); + waitForAlerts(); - cy.get(NUMBER_OF_SIGNALS) + cy.get(NUMBER_OF_ALERTS) .invoke('text') - .should('eql', numberOfSignalsToBeOpened.toString()); - cy.get(SHOWING_SIGNALS) + .should('eql', numberOfAlertsToBeOpened.toString()); + cy.get(SHOWING_ALERTS) .invoke('text') - .should('eql', `Showing ${numberOfSignalsToBeOpened.toString()} signal`); - cy.get(SIGNALS).should('have.length', numberOfSignalsToBeOpened); + .should('eql', `Showing ${numberOfAlertsToBeOpened.toString()} alert`); + cy.get(ALERTS).should('have.length', numberOfAlertsToBeOpened); }); }); }); diff --git a/x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts b/x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts index 6ea34f5203adc..d3ddb2ad71e30 100644 --- a/x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SIGNAL_ID } from '../screens/detections'; +import { ALERT_ID } from '../screens/detections'; import { PROVIDER_BADGE } from '../screens/timeline'; import { - expandFirstSignal, - investigateFirstSignalInTimeline, - waitForSignalsPanelToBeLoaded, + expandFirstAlert, + investigateFirstAlertInTimeline, + waitForAlertsPanelToBeLoaded, } from '../tasks/detections'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPage } from '../tasks/login'; @@ -19,22 +19,22 @@ import { DETECTIONS } from '../urls/navigation'; describe('Detections timeline', () => { beforeEach(() => { - esArchiverLoad('timeline_signals'); + esArchiverLoad('timeline_alerts'); loginAndWaitForPage(DETECTIONS); }); afterEach(() => { - esArchiverUnload('timeline_signals'); + esArchiverUnload('timeline_alerts'); }); - it('Investigate signal in default timeline', () => { - waitForSignalsPanelToBeLoaded(); - expandFirstSignal(); - cy.get(SIGNAL_ID) + it('Investigate alert in default timeline', () => { + waitForAlertsPanelToBeLoaded(); + expandFirstAlert(); + cy.get(ALERT_ID) .first() .invoke('text') .then((eventId) => { - investigateFirstSignalInTimeline(); + investigateFirstAlertInTimeline(); cy.get(PROVIDER_BADGE).invoke('text').should('eql', `_id: "${eventId}"`); }); }); diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index d07850e23f05e..e8f9411c149d4 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -10,12 +10,12 @@ import { RULE_SWITCH, SECOND_RULE, SEVENTH_RULE, -} from '../screens/signal_detection_rules'; +} from '../screens/alert_detection_rules'; import { - goToManageSignalDetectionRules, - waitForSignalsPanelToBeLoaded, - waitForSignalsIndexToBeCreated, + goToManageAlertDetectionRules, + waitForAlertsPanelToBeLoaded, + waitForAlertsIndexToBeCreated, } from '../tasks/detections'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; @@ -24,11 +24,11 @@ import { sortByActivatedRules, waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForRuleToBeActivated, -} from '../tasks/signal_detection_rules'; +} from '../tasks/alert_detection_rules'; import { DETECTIONS } from '../urls/navigation'; -describe('Signal detection rules', () => { +describe('Detection rules', () => { before(() => { esArchiverLoad('prebuilt_rules_loaded'); }); @@ -39,9 +39,9 @@ describe('Signal detection rules', () => { it('Sorts by activated rules', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); cy.get(RULE_NAME) .eq(FIFTH_RULE) diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts index 04762bbf352d2..e5cec16c48a37 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts @@ -36,7 +36,7 @@ import { RULES_TABLE, SEVERITY, SHOWING_RULES_TEXT, -} from '../screens/signal_detection_rules'; +} from '../screens/alert_detection_rules'; import { createAndActivateRule, @@ -44,9 +44,9 @@ import { fillDefineCustomRuleWithImportedQueryAndContinue, } from '../tasks/create_new_rule'; import { - goToManageSignalDetectionRules, - waitForSignalsIndexToBeCreated, - waitForSignalsPanelToBeLoaded, + goToManageAlertDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, } from '../tasks/detections'; import { changeToThreeHundredRowsPerPage, @@ -58,13 +58,13 @@ import { selectNumberOfRules, waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForRulesToBeLoaded, -} from '../tasks/signal_detection_rules'; +} from '../tasks/alert_detection_rules'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; -describe('Signal detection rules, custom', () => { +describe('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); @@ -75,9 +75,9 @@ describe('Signal detection rules, custom', () => { it('Creates and activates a new custom rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newRule); @@ -170,9 +170,9 @@ describe('Deletes custom rules', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); }); after(() => { diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts index aa1a111102160..4a12990438999 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_export.spec.ts @@ -5,13 +5,13 @@ */ import { - goToManageSignalDetectionRules, - waitForSignalsIndexToBeCreated, - waitForSignalsPanelToBeLoaded, + goToManageAlertDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, } from '../tasks/detections'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { exportFirstRule } from '../tasks/signal_detection_rules'; +import { exportFirstRule } from '../tasks/alert_detection_rules'; import { DETECTIONS } from '../urls/navigation'; @@ -33,9 +33,9 @@ describe('Export rules', () => { it('Exports a custom rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); exportFirstRule(); cy.wait('@export').then((xhr) => { cy.readFile(EXPECTED_EXPORTED_RULE_FILE_PATH).then(($expectedExportedJson) => { diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts index cb04d8117a923..fd2dff27ad359 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts @@ -34,7 +34,7 @@ import { RULES_ROW, RULES_TABLE, SEVERITY, -} from '../screens/signal_detection_rules'; +} from '../screens/alert_detection_rules'; import { createAndActivateRule, @@ -43,9 +43,9 @@ import { selectMachineLearningRuleType, } from '../tasks/create_new_rule'; import { - goToManageSignalDetectionRules, - waitForSignalsIndexToBeCreated, - waitForSignalsPanelToBeLoaded, + goToManageAlertDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, } from '../tasks/detections'; import { changeToThreeHundredRowsPerPage, @@ -54,13 +54,13 @@ import { goToRuleDetails, waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForRulesToBeLoaded, -} from '../tasks/signal_detection_rules'; +} from '../tasks/alert_detection_rules'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; -describe('Signal detection rules, machine learning', () => { +describe('Detection rules, machine learning', () => { before(() => { esArchiverLoad('prebuilt_rules_loaded'); }); @@ -71,9 +71,9 @@ describe('Signal detection rules, machine learning', () => { it('Creates and activates a new ml rule', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); goToCreateNewRule(); selectMachineLearningRuleType(); diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts index 005e24dad2a16..2cd087b2ca5e1 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts @@ -10,7 +10,7 @@ import { RELOAD_PREBUILT_RULES_BTN, RULES_ROW, RULES_TABLE, -} from '../screens/signal_detection_rules'; +} from '../screens/alert_detection_rules'; import { changeToThreeHundredRowsPerPage, @@ -22,11 +22,11 @@ import { waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForPrebuiltDetectionRulesToBeLoaded, waitForRulesToBeLoaded, -} from '../tasks/signal_detection_rules'; +} from '../tasks/alert_detection_rules'; import { - goToManageSignalDetectionRules, - waitForSignalsIndexToBeCreated, - waitForSignalsPanelToBeLoaded, + goToManageAlertDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, } from '../tasks/detections'; import { esArchiverLoadEmptyKibana, esArchiverUnloadEmptyKibana } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; @@ -35,7 +35,7 @@ import { DETECTIONS } from '../urls/navigation'; import { totalNumberOfPrebuiltRules } from '../objects/rule'; -describe('Signal detection rules, prebuilt rules', () => { +describe('Detection rules, prebuilt rules', () => { before(() => { esArchiverLoadEmptyKibana(); }); @@ -49,9 +49,9 @@ describe('Signal detection rules, prebuilt rules', () => { const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); @@ -74,9 +74,9 @@ describe('Deleting prebuilt rules', () => { esArchiverLoadEmptyKibana(); loginAndWaitForPageWithoutDateRange(DETECTIONS); - waitForSignalsPanelToBeLoaded(); - waitForSignalsIndexToBeCreated(); - goToManageSignalDetectionRules(); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); diff --git a/x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts b/x-pack/plugins/siem/cypress/screens/alert_detection_rules.ts similarity index 100% rename from x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts rename to x-pack/plugins/siem/cypress/screens/alert_detection_rules.ts diff --git a/x-pack/plugins/siem/cypress/screens/detections.ts b/x-pack/plugins/siem/cypress/screens/detections.ts index d9ffa5b5a4ab2..b915bcba2f880 100644 --- a/x-pack/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/plugins/siem/cypress/screens/detections.ts @@ -4,30 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -export const CLOSED_SIGNALS_BTN = '[data-test-subj="closedSignals"]'; +export const CLOSED_ALERTS_BTN = '[data-test-subj="closedAlerts"]'; -export const EXPAND_SIGNAL_BTN = '[data-test-subj="expand-event"]'; +export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; -export const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; +export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; -export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; +export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; -export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; +export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; -export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; +export const OPEN_CLOSE_ALERT_BTN = '[data-test-subj="update-alert-status-button"]'; -export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; +export const OPEN_CLOSE_ALERTS_BTN = '[data-test-subj="openCloseAlert"] button'; -export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; +export const OPENED_ALERTS_BTN = '[data-test-subj="openAlerts"]'; -export const SELECTED_SIGNALS = '[data-test-subj="selectedSignals"]'; +export const SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]'; -export const SEND_SIGNAL_TO_TIMELINE_BTN = '[data-test-subj="send-signal-to-timeline-button"]'; +export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]'; -export const SHOWING_SIGNALS = '[data-test-subj="showingSignals"]'; +export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]'; -export const SIGNALS = '[data-test-subj="event"]'; +export const ALERTS = '[data-test-subj="event"]'; -export const SIGNAL_ID = '[data-test-subj="draggable-content-_id"]'; +export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; -export const SIGNAL_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; +export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; diff --git a/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts b/x-pack/plugins/siem/cypress/tasks/alert_detection_rules.ts similarity index 98% rename from x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts rename to x-pack/plugins/siem/cypress/tasks/alert_detection_rules.ts index 6b2c4644a95d1..9710e0e808ac5 100644 --- a/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts +++ b/x-pack/plugins/siem/cypress/tasks/alert_detection_rules.ts @@ -24,7 +24,7 @@ import { SORT_RULES_BTN, THREE_HUNDRED_ROWS, EXPORT_ACTION_BTN, -} from '../screens/signal_detection_rules'; +} from '../screens/alert_detection_rules'; export const activateRule = (rulePosition: number) => { cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true }); diff --git a/x-pack/plugins/siem/cypress/tasks/detections.ts b/x-pack/plugins/siem/cypress/tasks/detections.ts index 9461dd5ff99cf..f53dd83635d85 100644 --- a/x-pack/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/plugins/siem/cypress/tasks/detections.ts @@ -5,66 +5,66 @@ */ import { - CLOSED_SIGNALS_BTN, - EXPAND_SIGNAL_BTN, - LOADING_SIGNALS_PANEL, - MANAGE_SIGNAL_DETECTION_RULES_BTN, - OPEN_CLOSE_SIGNAL_BTN, - OPEN_CLOSE_SIGNALS_BTN, - OPENED_SIGNALS_BTN, - SEND_SIGNAL_TO_TIMELINE_BTN, - SIGNALS, - SIGNAL_CHECKBOX, + CLOSED_ALERTS_BTN, + EXPAND_ALERT_BTN, + LOADING_ALERTS_PANEL, + MANAGE_ALERT_DETECTION_RULES_BTN, + OPEN_CLOSE_ALERT_BTN, + OPEN_CLOSE_ALERTS_BTN, + OPENED_ALERTS_BTN, + SEND_ALERT_TO_TIMELINE_BTN, + ALERTS, + ALERT_CHECKBOX, } from '../screens/detections'; import { REFRESH_BUTTON } from '../screens/siem_header'; -export const closeFirstSignal = () => { - cy.get(OPEN_CLOSE_SIGNAL_BTN).first().click({ force: true }); +export const closeFirstAlert = () => { + cy.get(OPEN_CLOSE_ALERT_BTN).first().click({ force: true }); }; -export const closeSignals = () => { - cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); +export const closeAlerts = () => { + cy.get(OPEN_CLOSE_ALERTS_BTN).click({ force: true }); }; -export const expandFirstSignal = () => { - cy.get(EXPAND_SIGNAL_BTN).first().click({ force: true }); +export const expandFirstAlert = () => { + cy.get(EXPAND_ALERT_BTN).first().click({ force: true }); }; -export const goToClosedSignals = () => { - cy.get(CLOSED_SIGNALS_BTN).click({ force: true }); +export const goToClosedAlerts = () => { + cy.get(CLOSED_ALERTS_BTN).click({ force: true }); }; -export const goToManageSignalDetectionRules = () => { - cy.get(MANAGE_SIGNAL_DETECTION_RULES_BTN).should('exist').click({ force: true }); +export const goToManageAlertDetectionRules = () => { + cy.get(MANAGE_ALERT_DETECTION_RULES_BTN).should('exist').click({ force: true }); }; -export const goToOpenedSignals = () => { - cy.get(OPENED_SIGNALS_BTN).click({ force: true }); +export const goToOpenedAlerts = () => { + cy.get(OPENED_ALERTS_BTN).click({ force: true }); }; -export const openFirstSignal = () => { - cy.get(OPEN_CLOSE_SIGNAL_BTN).first().click({ force: true }); +export const openFirstAlert = () => { + cy.get(OPEN_CLOSE_ALERT_BTN).first().click({ force: true }); }; -export const openSignals = () => { - cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); +export const openAlerts = () => { + cy.get(OPEN_CLOSE_ALERTS_BTN).click({ force: true }); }; -export const selectNumberOfSignals = (numberOfSignals: number) => { - for (let i = 0; i < numberOfSignals; i++) { - cy.get(SIGNAL_CHECKBOX).eq(i).click({ force: true }); +export const selectNumberOfAlerts = (numberOfAlerts: number) => { + for (let i = 0; i < numberOfAlerts; i++) { + cy.get(ALERT_CHECKBOX).eq(i).click({ force: true }); } }; -export const investigateFirstSignalInTimeline = () => { - cy.get(SEND_SIGNAL_TO_TIMELINE_BTN).first().click({ force: true }); +export const investigateFirstAlertInTimeline = () => { + cy.get(SEND_ALERT_TO_TIMELINE_BTN).first().click({ force: true }); }; -export const waitForSignals = () => { +export const waitForAlerts = () => { cy.get(REFRESH_BUTTON).invoke('text').should('not.equal', 'Updating'); }; -export const waitForSignalsIndexToBeCreated = () => { +export const waitForAlertsIndexToBeCreated = () => { cy.request({ url: '/api/detection_engine/index', retryOnStatusCodeFailure: true }).then( (response) => { if (response.status !== 200) { @@ -74,12 +74,12 @@ export const waitForSignalsIndexToBeCreated = () => { ); }; -export const waitForSignalsPanelToBeLoaded = () => { - cy.get(LOADING_SIGNALS_PANEL).should('exist'); - cy.get(LOADING_SIGNALS_PANEL).should('not.exist'); +export const waitForAlertsPanelToBeLoaded = () => { + cy.get(LOADING_ALERTS_PANEL).should('exist'); + cy.get(LOADING_ALERTS_PANEL).should('not.exist'); }; -export const waitForSignalsToBeLoaded = () => { - const expectedNumberOfDisplayedSignals = 25; - cy.get(SIGNALS).should('have.length', expectedNumberOfDisplayedSignals); +export const waitForAlertsToBeLoaded = () => { + const expectedNumberOfDisplayedAlerts = 25; + cy.get(ALERTS).should('have.length', expectedNumberOfDisplayedAlerts); }; diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx similarity index 84% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx index f921c00cdafb7..7f340b0bea37b 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx @@ -7,14 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SignalsHistogram } from './signals_histogram'; +import { AlertsHistogram } from './alerts_histogram'; jest.mock('../../../common/lib/kibana'); -describe('SignalsHistogram', () => { +describe('AlertsHistogram', () => { it('renders correctly', () => { const wrapper = shallow( - ( +export const AlertsHistogram = React.memo( ({ chartHeight = DEFAULT_CHART_HEIGHT, data, @@ -48,9 +48,9 @@ export const SignalsHistogram = React.memo( const theme = useTheme(); const chartSize: ChartSizeArray = useMemo(() => ['100%', chartHeight], [chartHeight]); - const xAxisId = 'signalsHistogramAxisX'; - const yAxisId = 'signalsHistogramAxisY'; - const id = 'signalsHistogram'; + const xAxisId = 'alertsHistogramAxisX'; + const yAxisId = 'alertsHistogramAxisY'; + const id = 'alertsHistogram'; const yAccessors = useMemo(() => ['y'], []); const splitSeriesAccessors = useMemo(() => ['g'], []); const tickFormat = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); @@ -59,7 +59,7 @@ export const SignalsHistogram = React.memo( <> {loading && ( ( } ); -SignalsHistogram.displayName = 'SignalsHistogram'; +AlertsHistogram.displayName = 'AlertsHistogram'; diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/config.ts b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/config.ts similarity index 88% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/config.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/config.ts index 2c5a1ddd9a010..5138835873812 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/config.ts +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/config.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalsHistogramOption } from './types'; +import { AlertsHistogramOption } from './types'; -export const signalsHistogramOptions: SignalsHistogramOption[] = [ +export const alertsHistogramOptions: AlertsHistogramOption[] = [ { text: 'signal.rule.risk_score', value: 'signal.rule.risk_score' }, { text: 'signal.rule.severity', value: 'signal.rule.severity' }, { text: 'signal.rule.threat.tactic.name', value: 'signal.rule.threat.tactic.name' }, diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.test.tsx similarity index 89% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.test.tsx index 2758625c0d4af..bfe4cee088a02 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.test.tsx @@ -9,25 +9,25 @@ import { showInitialLoadingSpinner } from './helpers'; describe('helpers', () => { describe('showInitialLoadingSpinner', () => { test('it should (only) show the spinner during initial loading, while we are fetching data', () => { - expect(showInitialLoadingSpinner({ isInitialLoading: true, isLoadingSignals: true })).toBe( + expect(showInitialLoadingSpinner({ isInitialLoading: true, isLoadingAlerts: true })).toBe( true ); }); test('it should STOP showing the spinner (during initial loading) when the first data fetch completes', () => { - expect(showInitialLoadingSpinner({ isInitialLoading: true, isLoadingSignals: false })).toBe( + expect(showInitialLoadingSpinner({ isInitialLoading: true, isLoadingAlerts: false })).toBe( false ); }); test('it should NOT show the spinner after initial loading has completed, even if the user requests more data (e.g. by clicking Refresh)', () => { - expect(showInitialLoadingSpinner({ isInitialLoading: false, isLoadingSignals: true })).toBe( + expect(showInitialLoadingSpinner({ isInitialLoading: false, isLoadingAlerts: true })).toBe( false ); }); test('it should NOT show the spinner after initial loading has completed', () => { - expect(showInitialLoadingSpinner({ isInitialLoading: false, isLoadingSignals: false })).toBe( + expect(showInitialLoadingSpinner({ isInitialLoading: false, isLoadingAlerts: false })).toBe( false ); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.tsx similarity index 67% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.tsx index 0c9fa39e53d00..9d124201f022e 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/helpers.tsx @@ -5,21 +5,19 @@ */ import { showAllOthersBucket } from '../../../../common/constants'; -import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; -import { SignalSearchResponse } from '../../containers/detection_engine/signals/types'; +import { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; +import { AlertSearchResponse } from '../../containers/detection_engine/alerts/types'; import * as i18n from './translations'; -export const formatSignalsData = ( - signalsData: SignalSearchResponse<{}, SignalsAggregation> | null -) => { - const groupBuckets: SignalsGroupBucket[] = - signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; - return groupBuckets.reduce((acc, { key: group, signals }) => { - const signalsBucket: SignalsBucket[] = signals.buckets ?? []; +export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggregation> | null) => { + const groupBuckets: AlertsGroupBucket[] = + alertsData?.aggregations?.alertsByGrouping?.buckets ?? []; + return groupBuckets.reduce((acc, { key: group, alerts }) => { + const alertsBucket: AlertsBucket[] = alerts.buckets ?? []; return [ ...acc, - ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ + ...alertsBucket.map(({ key, doc_count }: AlertsBucket) => ({ x: key, y: doc_count, g: group, @@ -28,7 +26,7 @@ export const formatSignalsData = ( }, []); }; -export const getSignalsHistogramQuery = ( +export const getAlertsHistogramQuery = ( stackByField: string, from: number, to: number, @@ -44,7 +42,7 @@ export const getSignalsHistogramQuery = ( return { aggs: { - signalsByGrouping: { + alertsByGrouping: { terms: { field: stackByField, ...missing, @@ -54,7 +52,7 @@ export const getSignalsHistogramQuery = ( size: 10, }, aggs: { - signals: { + alerts: { date_histogram: { field: '@timestamp', fixed_interval: `${Math.floor((to - from) / 32)}ms`, @@ -87,15 +85,15 @@ export const getSignalsHistogramQuery = ( }; /** - * Returns `true` when the signals histogram initial loading spinner should be shown + * Returns `true` when the alerts histogram initial loading spinner should be shown * * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed - * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) + * @param isLoadingAlerts When `true`, IO is being performed to request alerts (for rendering in the histogram) */ export const showInitialLoadingSpinner = ({ isInitialLoading, - isLoadingSignals, + isLoadingAlerts, }: { isInitialLoading: boolean; - isLoadingSignals: boolean; -}): boolean => isInitialLoading && isLoadingSignals; + isLoadingAlerts: boolean; +}): boolean => isInitialLoading && isLoadingAlerts; diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.test.tsx similarity index 85% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.test.tsx index 6578af19094df..3376df76ac6ec 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/index.test.tsx @@ -7,15 +7,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SignalsHistogramPanel } from './index'; +import { AlertsHistogramPanel } from './index'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/navigation/use_get_url_search'); -describe('SignalsHistogramPanel', () => { +describe('AlertsHistogramPanel', () => { it('renders correctly', () => { const wrapper = shallow( - ` position: relative; `; -const defaultTotalSignalsObj: SignalsTotal = { +const defaultTotalAlertsObj: AlertsTotal = { value: 0, relation: 'eq', }; export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; -const ViewSignalsFlexItem = styled(EuiFlexItem)` +const ViewAlertsFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; -interface SignalsHistogramPanelProps { +interface AlertsHistogramPanelProps { chartHeight?: number; - defaultStackByOption?: SignalsHistogramOption; + defaultStackByOption?: AlertsHistogramOption; deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; @@ -66,9 +66,9 @@ interface SignalsHistogramPanelProps { panelHeight?: number; signalIndexName: string | null; setQuery: (params: RegisterQuery) => void; - showLinkToSignals?: boolean; - showTotalSignalsCount?: boolean; - stackByOptions?: SignalsHistogramOption[]; + showLinkToAlerts?: boolean; + showTotalAlertsCount?: boolean; + stackByOptions?: AlertsHistogramOption[]; title?: string; to: number; updateDateRange: UpdateDateRange; @@ -81,10 +81,10 @@ const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ const NO_LEGEND_DATA: LegendItem[] = []; -export const SignalsHistogramPanel = memo( +export const AlertsHistogramPanel = memo( ({ chartHeight, - defaultStackByOption = signalsHistogramOptions[0], + defaultStackByOption = alertsHistogramOptions[0], deleteQuery, filters, headerChildren, @@ -95,8 +95,8 @@ export const SignalsHistogramPanel = memo( panelHeight = DEFAULT_PANEL_HEIGHT, setQuery, signalIndexName, - showLinkToSignals = false, - showTotalSignalsCount = false, + showLinkToAlerts = false, + showTotalAlertsCount = false, stackByOptions, to, title = i18n.HISTOGRAM_HEADER, @@ -106,32 +106,32 @@ export const SignalsHistogramPanel = memo( const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); const [isInitialLoading, setIsInitialLoading] = useState(true); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const [totalSignalsObj, setTotalSignalsObj] = useState(defaultTotalSignalsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState( + const [totalAlertsObj, setTotalAlertsObj] = useState(defaultTotalAlertsObj); + const [selectedStackByOption, setSelectedStackByOption] = useState( onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) ); const { - loading: isLoadingSignals, - data: signalsData, - setQuery: setSignalsQuery, + loading: isLoadingAlerts, + data: alertsData, + setQuery: setAlertsQuery, response, request, refetch, - } = useQuerySignals<{}, SignalsAggregation>( - getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), + } = useQueryAlerts<{}, AlertsAggregation>( + getAlertsHistogramQuery(selectedStackByOption.value, from, to, []), signalIndexName ); const kibana = useKibana(); const urlSearch = useGetUrlSearch(navTabs.detections); - const totalSignals = useMemo( + const totalAlerts = useMemo( () => - i18n.SHOWING_SIGNALS( - numeral(totalSignalsObj.value).format(defaultNumberFormat), - totalSignalsObj.value, - totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' + i18n.SHOWING_ALERTS( + numeral(totalAlertsObj.value).format(defaultNumberFormat), + totalAlertsObj.value, + totalAlertsObj.relation === 'gte' ? '>' : totalAlertsObj.relation === 'lte' ? '<' : '' ), - [totalSignalsObj] + [totalAlertsObj] ); const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => { @@ -140,12 +140,12 @@ export const SignalsHistogramPanel = memo( ); }, []); - const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + const formattedAlertsData = useMemo(() => formatAlertsData(alertsData), [alertsData]); const legendItems: LegendItem[] = useMemo( () => - signalsData?.aggregations?.signalsByGrouping?.buckets != null - ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ + alertsData?.aggregations?.alertsByGrouping?.buckets != null + ? alertsData.aggregations.alertsByGrouping.buckets.map((bucket, i) => ({ color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, dataProviderId: escapeDataProviderId( `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` @@ -154,20 +154,20 @@ export const SignalsHistogramPanel = memo( value: bucket.key, })) : NO_LEGEND_DATA, - [signalsData, selectedStackByOption.value] + [alertsData, selectedStackByOption.value] ); useEffect(() => { let canceled = false; - if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { + if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingAlerts })) { setIsInitialLoading(false); } return () => { canceled = true; // prevent long running data fetches from updating state after unmounting }; - }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); + }, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]); useEffect(() => { return () => { @@ -185,20 +185,20 @@ export const SignalsHistogramPanel = memo( dsl: [request], response: [response], }, - loading: isLoadingSignals, + loading: isLoadingAlerts, refetch, }); } - }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); + }, [setQuery, isLoadingAlerts, alertsData, response, request, refetch]); useEffect(() => { - setTotalSignalsObj( - signalsData?.hits.total ?? { + setTotalAlertsObj( + alertsData?.hits.total ?? { value: 0, relation: 'eq', } ); - }, [signalsData]); + }, [alertsData]); useEffect(() => { const converted = esQuery.buildEsQuery( @@ -211,8 +211,8 @@ export const SignalsHistogramPanel = memo( } ); - setSignalsQuery( - getSignalsHistogramQuery( + setAlertsQuery( + getAlertsHistogramQuery( selectedStackByOption.value, from, to, @@ -222,14 +222,14 @@ export const SignalsHistogramPanel = memo( }, [selectedStackByOption.value, from, to, query, filters]); const linkButton = useMemo(() => { - if (showLinkToSignals) { + if (showLinkToAlerts) { return ( - - {i18n.VIEW_SIGNALS} - + + {i18n.VIEW_ALERTS} + ); } - }, [showLinkToSignals, urlSearch]); + }, [showLinkToAlerts, urlSearch]); const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ onlyField, @@ -237,13 +237,13 @@ export const SignalsHistogramPanel = memo( ]); return ( - + @@ -264,13 +264,13 @@ export const SignalsHistogramPanel = memo( {isInitialLoading ? ( ) : ( - @@ -281,4 +281,4 @@ export const SignalsHistogramPanel = memo( } ); -SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; +AlertsHistogramPanel.displayName = 'AlertsHistogramPanel'; diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/translations.ts b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/translations.ts similarity index 50% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/translations.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/translations.ts index e7b76a48c7592..91345e3d989f1 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/translations.ts +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/translations.ts @@ -7,116 +7,116 @@ import { i18n } from '@kbn/i18n'; export const STACK_BY_LABEL = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.stackByLabel', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.stackByLabel', { defaultMessage: 'Stack by', } ); export const STACK_BY_RISK_SCORES = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.riskScoresDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown', { defaultMessage: 'Risk scores', } ); export const STACK_BY_SEVERITIES = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.severitiesDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown', { defaultMessage: 'Severities', } ); export const STACK_BY_DESTINATION_IPS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.destinationIpsDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown', { defaultMessage: 'Top destination IPs', } ); export const STACK_BY_SOURCE_IPS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.sourceIpsDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown', { defaultMessage: 'Top source IPs', } ); export const STACK_BY_ACTIONS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventActionsDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown', { defaultMessage: 'Top event actions', } ); export const STACK_BY_CATEGORIES = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventCategoriesDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown', { defaultMessage: 'Top event categories', } ); export const STACK_BY_HOST_NAMES = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.hostNamesDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown', { defaultMessage: 'Top host names', } ); export const STACK_BY_RULE_TYPES = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.ruleTypesDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown', { defaultMessage: 'Top rule types', } ); export const STACK_BY_RULE_NAMES = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.rulesDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown', { defaultMessage: 'Top rules', } ); export const STACK_BY_USERS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.usersDropDown', + 'xpack.siem.detectionEngine.alerts.histogram.stackByOptions.usersDropDown', { defaultMessage: 'Top users', } ); export const TOP = (fieldName: string) => - i18n.translate('xpack.siem.detectionEngine.signals.histogram.topNLabel', { + i18n.translate('xpack.siem.detectionEngine.alerts.histogram.topNLabel', { values: { fieldName }, defaultMessage: `Top {fieldName}`, }); export const HISTOGRAM_HEADER = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.headerTitle', + 'xpack.siem.detectionEngine.alerts.histogram.headerTitle', { - defaultMessage: 'Signal count', + defaultMessage: 'Alert count', } ); export const ALL_OTHERS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel', + 'xpack.siem.detectionEngine.alerts.histogram.allOthersGroupingLabel', { defaultMessage: 'All others', } ); -export const VIEW_SIGNALS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.viewSignalsButtonLabel', +export const VIEW_ALERTS = i18n.translate( + 'xpack.siem.detectionEngine.alerts.histogram.viewAlertsButtonLabel', { - defaultMessage: 'View signals', + defaultMessage: 'View alerts', } ); -export const SHOWING_SIGNALS = ( - totalSignalsFormatted: string, - totalSignals: number, +export const SHOWING_ALERTS = ( + totalAlertsFormatted: string, + totalAlerts: number, modifier: string ) => - i18n.translate('xpack.siem.detectionEngine.signals.histogram.showingSignalsTitle', { - values: { totalSignalsFormatted, totalSignals, modifier }, + i18n.translate('xpack.siem.detectionEngine.alerts.histogram.showingAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts, modifier }, defaultMessage: - 'Showing: {modifier}{totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}', + 'Showing: {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/types.ts b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/types.ts similarity index 70% rename from x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/types.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/types.ts index 41d58a4a7391d..0bf483f7ec927 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/types.ts +++ b/x-pack/plugins/siem/public/alerts/components/alerts_histogram_panel/types.ts @@ -6,7 +6,7 @@ import { inputsModel } from '../../../common/store'; -export interface SignalsHistogramOption { +export interface AlertsHistogramOption { text: string; value: string; } @@ -17,26 +17,26 @@ export interface HistogramData { g: string; } -export interface SignalsAggregation { - signalsByGrouping: { - buckets: SignalsGroupBucket[]; +export interface AlertsAggregation { + alertsByGrouping: { + buckets: AlertsGroupBucket[]; }; } -export interface SignalsBucket { +export interface AlertsBucket { key_as_string: string; key: number; doc_count: number; } -export interface SignalsGroupBucket { +export interface AlertsGroupBucket { key: string; - signals: { - buckets: SignalsBucket[]; + alerts: { + buckets: AlertsBucket[]; }; } -export interface SignalsTotal { +export interface AlertsTotal { value: number; relation: string; } diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_info/index.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_info/index.tsx new file mode 100644 index 0000000000000..7d35e429bcf50 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/alerts_info/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n/react'; +import React, { useState, useEffect } from 'react'; + +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { buildLastAlertsQuery } from './query.dsl'; +import { Aggs } from './types'; + +interface AlertInfo { + ruleId?: string | null; +} + +type Return = [React.ReactNode, React.ReactNode]; + +export const useAlertInfo = ({ ruleId = null }: AlertInfo): Return => { + const [lastAlerts, setLastAlerts] = useState( + + ); + const [totalAlerts, setTotalAlerts] = useState( + + ); + + const { loading, data: alerts } = useQueryAlerts(buildLastAlertsQuery(ruleId)); + + useEffect(() => { + if (alerts != null) { + const myAlerts = alerts; + setLastAlerts( + myAlerts.aggregations?.lastSeen.value != null ? ( + + ) : null + ); + setTotalAlerts(<>{myAlerts.hits.total.value}); + } else { + setLastAlerts(null); + setTotalAlerts(null); + } + }, [loading, alerts]); + + return [lastAlerts, totalAlerts]; +}; diff --git a/x-pack/plugins/siem/public/alerts/components/signals_info/query.dsl.ts b/x-pack/plugins/siem/public/alerts/components/alerts_info/query.dsl.ts similarity index 91% rename from x-pack/plugins/siem/public/alerts/components/signals_info/query.dsl.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_info/query.dsl.ts index 8cb07a4f8e6b5..a3972fd35bf2d 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals_info/query.dsl.ts +++ b/x-pack/plugins/siem/public/alerts/components/alerts_info/query.dsl.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const buildLastSignalsQuery = (ruleId: string | undefined | null) => { +export const buildLastAlertsQuery = (ruleId: string | undefined | null) => { const queryFilter = [ { bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 }, diff --git a/x-pack/plugins/siem/public/alerts/components/signals_info/types.ts b/x-pack/plugins/siem/public/alerts/components/alerts_info/types.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/signals_info/types.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_info/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/actions.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/alerts/components/signals/actions.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/actions.test.tsx index d7a8a55077340..2fa7cfeedcd15 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/actions.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/actions.test.tsx @@ -6,9 +6,9 @@ import sinon from 'sinon'; import moment from 'moment'; -import { sendSignalToTimelineAction, determineToAndFrom } from './actions'; +import { sendAlertToTimelineAction, determineToAndFrom } from './actions'; import { - mockEcsDataWithSignal, + mockEcsDataWithAlert, defaultTimelineProps, apolloClient, mockTimelineApolloResult, @@ -19,7 +19,7 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline' jest.mock('apollo-client'); -describe('signals actions', () => { +describe('alert actions', () => { const anchor = '2020-03-01T17:59:46.349Z'; const unix = moment(anchor).valueOf(); let createTimeline: CreateTimeline; @@ -46,13 +46,13 @@ describe('signals actions', () => { clock.restore(); }); - describe('sendSignalToTimelineAction', () => { + describe('sendAlertToTimelineAction', () => { describe('timeline id is NOT empty string and apollo client exists', () => { test('it invokes updateTimelineIsLoading to set to true', async () => { - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ apolloClient, createTimeline, - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, }); @@ -61,10 +61,10 @@ describe('signals actions', () => { }); test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ apolloClient, createTimeline, - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, }); const expected = { @@ -246,10 +246,10 @@ describe('signals actions', () => { }; jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ apolloClient, createTimeline, - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, }); // @ts-ignore @@ -275,10 +275,10 @@ describe('signals actions', () => { }; jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ apolloClient, createTimeline, - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, }); // @ts-ignore @@ -293,10 +293,10 @@ describe('signals actions', () => { throw new Error('Test error'); }); - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ apolloClient, createTimeline, - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, updateTimelineIsLoading, }); @@ -313,16 +313,16 @@ describe('signals actions', () => { describe('timelineId is empty string', () => { test('it invokes createTimeline with timelineDefaults', async () => { const ecsDataMock: Ecs = { - ...mockEcsDataWithSignal, + ...mockEcsDataWithAlert, signal: { rule: { - ...mockEcsDataWithSignal.signal?.rule!, + ...mockEcsDataWithAlert.signal?.rule!, timeline_id: null, }, }, }; - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ apolloClient, createTimeline, ecsData: ecsDataMock, @@ -338,16 +338,16 @@ describe('signals actions', () => { describe('apolloClient is not defined', () => { test('it invokes createTimeline with timelineDefaults', async () => { const ecsDataMock: Ecs = { - ...mockEcsDataWithSignal, + ...mockEcsDataWithAlert, signal: { rule: { - ...mockEcsDataWithSignal.signal?.rule!, + ...mockEcsDataWithAlert.signal?.rule!, timeline_id: [''], }, }, }; - await sendSignalToTimelineAction({ + await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, updateTimelineIsLoading, @@ -363,7 +363,7 @@ describe('signals actions', () => { describe('determineToAndFrom', () => { test('it uses ecs.Data.timestamp if one is provided', () => { const ecsDataMock: Ecs = { - ...mockEcsDataWithSignal, + ...mockEcsDataWithAlert, timestamp: '2020-03-20T17:59:46.349Z', }; const result = determineToAndFrom({ ecsData: ecsDataMock }); @@ -374,7 +374,7 @@ describe('signals actions', () => { test('it uses current time timestamp if ecsData.timestamp is not provided', () => { const { timestamp, ...ecsDataMock } = { - ...mockEcsDataWithSignal, + ...mockEcsDataWithAlert, }; const result = determineToAndFrom({ ecsData: ecsDataMock }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/actions.tsx similarity index 82% rename from x-pack/plugins/siem/public/alerts/components/signals/actions.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/actions.tsx index f01b39ccaba0d..cde81d44bc5d6 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/actions.tsx @@ -8,8 +8,8 @@ import dateMath from '@elastic/datemath'; import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; -import { updateSignalStatus } from '../../containers/detection_engine/signals/api'; -import { SendSignalToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; +import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; +import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../graphql/types'; import { oneTimelineQuery } from '../../../timelines/containers/one/index.gql_query'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -24,7 +24,7 @@ import { replaceTemplateFieldFromDataProviders, } from './helpers'; -export const getUpdateSignalsQuery = (eventIds: Readonly) => { +export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { query: { bool: { @@ -44,35 +44,35 @@ export const getFilterAndRuleBounds = ( const stringFilter = data?.[0].filter((d) => d.field === 'signal.rule.filters')?.[0]?.value ?? []; const eventTimes = data - .flatMap((signal) => signal.filter((d) => d.field === 'signal.original_time')?.[0]?.value ?? []) + .flatMap((alert) => alert.filter((d) => d.field === 'signal.original_time')?.[0]?.value ?? []) .map((d) => moment(d)); return [stringFilter, moment.min(eventTimes).valueOf(), moment.max(eventTimes).valueOf()]; }; -export const updateSignalStatusAction = async ({ +export const updateAlertStatusAction = async ({ query, - signalIds, + alertIds, status, setEventsLoading, setEventsDeleted, onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, -}: UpdateSignalStatusActionProps) => { +}: UpdateAlertStatusActionProps) => { try { - setEventsLoading({ eventIds: signalIds, isLoading: true }); + setEventsLoading({ eventIds: alertIds, isLoading: true }); - const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds); + const queryObject = query ? { query: JSON.parse(query) } : getUpdateAlertsQuery(alertIds); - const response = await updateSignalStatus({ query: queryObject, status }); + const response = await updateAlertStatus({ query: queryObject, status }); // TODO: Only delete those that were successfully updated from updatedRules - setEventsDeleted({ eventIds: signalIds, isDeleted: true }); + setEventsDeleted({ eventIds: alertIds, isDeleted: true }); onAlertStatusUpdateSuccess(response.updated, status); } catch (error) { onAlertStatusUpdateFailure(status, error); } finally { - setEventsLoading({ eventIds: signalIds, isLoading: false }); + setEventsLoading({ eventIds: alertIds, isLoading: false }); } }; @@ -91,13 +91,13 @@ export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { return { to, from }; }; -export const sendSignalToTimelineAction = async ({ +export const sendAlertToTimelineAction = async ({ apolloClient, createTimeline, ecsData, updateTimelineIsLoading, -}: SendSignalToTimelineActionProps) => { - let openSignalInBasicTimeline = true; +}: SendAlertToTimelineActionProps) => { + let openAlertInBasicTimeline = true; const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : ''; const timelineId = ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; @@ -120,7 +120,7 @@ export const sendSignalToTimelineAction = async ({ if (!isEmpty(resultingTimeline)) { const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); - openSignalInBasicTimeline = false; + openAlertInBasicTimeline = false; const { timeline } = formatTimelineResultToModel(timelineTemplate, true); const query = replaceTemplateFieldFromQuery( timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', @@ -162,12 +162,12 @@ export const sendSignalToTimelineAction = async ({ }); } } catch { - openSignalInBasicTimeline = true; + openAlertInBasicTimeline = true; updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); } } - if (openSignalInBasicTimeline) { + if (openAlertInBasicTimeline) { createTimeline({ from, timeline: { @@ -175,7 +175,7 @@ export const sendSignalToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx similarity index 68% rename from x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx index dd30bb1b0a74d..d7fabdabf8225 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SignalsTableFilterGroup } from './index'; +import { AlertsTableFilterGroup } from './index'; -describe('SignalsTableFilterGroup', () => { +describe('AlertsTableFilterGroup', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('EuiFilterButton')).toBeTruthy(); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.tsx similarity index 74% rename from x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.tsx index a8dd22863e3c9..8521170637d6f 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_filter_group/index.tsx @@ -10,13 +10,13 @@ import * as i18n from '../translations'; export const FILTER_OPEN = 'open'; export const FILTER_CLOSED = 'closed'; -export type SignalFilterOption = typeof FILTER_OPEN | typeof FILTER_CLOSED; +export type AlertFilterOption = typeof FILTER_OPEN | typeof FILTER_CLOSED; interface Props { - onFilterGroupChanged: (filterGroup: SignalFilterOption) => void; + onFilterGroupChanged: (filterGroup: AlertFilterOption) => void; } -const SignalsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }) => { +const AlertsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }) => { const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const onClickOpenFilterCallback = useCallback(() => { @@ -32,23 +32,23 @@ const SignalsTableFilterGroupComponent: React.FC = ({ onFilterGroupChange return ( - {i18n.OPEN_SIGNALS} + {i18n.OPEN_ALERTS} - {i18n.CLOSED_SIGNALS} + {i18n.CLOSED_ALERTS} ); }; -export const SignalsTableFilterGroup = React.memo(SignalsTableFilterGroupComponent); +export const AlertsTableFilterGroup = React.memo(AlertsTableFilterGroupComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx similarity index 76% rename from x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx index 3b43185c2c16b..543e11c9b1e69 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -7,14 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SignalsUtilityBar } from './index'; +import { AlertsUtilityBar } from './index'; jest.mock('../../../../common/lib/kibana'); -describe('SignalsUtilityBar', () => { +describe('AlertsUtilityBar', () => { it('renders correctly', () => { const wrapper = shallow( - { isFilteredToOpen={false} selectAll={jest.fn()} showClearSelection={true} - updateSignalsStatus={jest.fn()} + updateAlertsStatus={jest.fn()} /> ); - expect(wrapper.find('[dataTestSubj="openCloseSignal"]')).toBeTruthy(); + expect(wrapper.find('[dataTestSubj="openCloseAlert"]')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx similarity index 77% rename from x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx index e23f4ebdd3d30..68b7039690db4 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx @@ -19,10 +19,10 @@ import { import * as i18n from './translations'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { TimelineNonEcsData } from '../../../../graphql/types'; -import { UpdateSignalsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; +import { UpdateAlertsStatus } from '../types'; +import { FILTER_CLOSED, FILTER_OPEN } from '../alerts_filter_group'; -interface SignalsUtilityBarProps { +interface AlertsUtilityBarProps { canUserCRUD: boolean; hasIndexWrite: boolean; areEventsLoading: boolean; @@ -32,10 +32,10 @@ interface SignalsUtilityBarProps { selectedEventIds: Readonly>; showClearSelection: boolean; totalCount: number; - updateSignalsStatus: UpdateSignalsStatus; + updateAlertsStatus: UpdateAlertsStatus; } -const SignalsUtilityBarComponent: React.FC = ({ +const AlertsUtilityBarComponent: React.FC = ({ canUserCRUD, hasIndexWrite, areEventsLoading, @@ -45,16 +45,16 @@ const SignalsUtilityBarComponent: React.FC = ({ isFilteredToOpen, selectAll, showClearSelection, - updateSignalsStatus, + updateAlertsStatus, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const handleUpdateStatus = useCallback(async () => { - await updateSignalsStatus({ - signalIds: Object.keys(selectedEventIds), + await updateAlertsStatus({ + alertIds: Object.keys(selectedEventIds), status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, }); - }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); + }, [selectedEventIds, updateAlertsStatus, isFilteredToOpen]); const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( @@ -66,25 +66,25 @@ const SignalsUtilityBarComponent: React.FC = ({ - - {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + + {i18n.SHOWING_ALERTS(formattedTotalCount, totalCount)} {canUserCRUD && hasIndexWrite && ( <> - - {i18n.SELECTED_SIGNALS( + + {i18n.SELECTED_ALERTS( showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, showClearSelection ? totalCount : Object.keys(selectedEventIds).length )} {isFilteredToOpen @@ -104,7 +104,7 @@ const SignalsUtilityBarComponent: React.FC = ({ > {showClearSelection ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + : i18n.SELECT_ALL_ALERTS(formattedTotalCount, totalCount)} )} @@ -115,8 +115,8 @@ const SignalsUtilityBarComponent: React.FC = ({ ); }; -export const SignalsUtilityBar = React.memo( - SignalsUtilityBarComponent, +export const AlertsUtilityBar = React.memo( + AlertsUtilityBarComponent, (prevProps, nextProps) => prevProps.areEventsLoading === nextProps.areEventsLoading && prevProps.selectedEventIds === nextProps.selectedEventIds && diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts new file mode 100644 index 0000000000000..ae5070efc21e1 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SHOWING_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.alerts.utilityBar.showingAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts }, + defaultMessage: + 'Showing {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); + +export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.alerts.utilityBar.selectedAlertsTitle', { + values: { selectedAlertsFormatted, selectedAlerts }, + defaultMessage: + 'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}', + }); + +export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.alerts.utilityBar.selectAllAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts }, + defaultMessage: + 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); + +export const CLEAR_SELECTION = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.clearSelectionTitle', + { + defaultMessage: 'Clear selection', + } +); + +export const BATCH_ACTIONS = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.batchActionsTitle', + { + defaultMessage: 'Batch actions', + } +); + +export const BATCH_ACTION_VIEW_SELECTED_IN_HOSTS = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.batchActions.viewSelectedInHostsTitle', + { + defaultMessage: 'View selected in hosts', + } +); + +export const BATCH_ACTION_VIEW_SELECTED_IN_NETWORK = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.batchActions.viewSelectedInNetworkTitle', + { + defaultMessage: 'View selected in network', + } +); + +export const BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.batchActions.viewSelectedInTimelineTitle', + { + defaultMessage: 'View selected in timeline', + } +); + +export const BATCH_ACTION_OPEN_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.batchActions.openSelectedTitle', + { + defaultMessage: 'Open selected', + } +); + +export const BATCH_ACTION_CLOSE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.alerts.utilityBar.batchActions.closeSelectedTitle', + { + defaultMessage: 'Close selected', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx similarity index 67% rename from x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx index 7821bfaaf9575..b191464984c53 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.test.tsx @@ -9,23 +9,23 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { TimelineAction } from '../../../timelines/components/timeline/body/actions'; -import { buildSignalsRuleIdFilter, getSignalsActions } from './default_config'; +import { buildAlertsRuleIdFilter, getAlertActions } from './default_config'; import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps, UpdateTimelineLoading, } from './types'; -import { mockEcsDataWithSignal } from '../../../common/mock/mock_ecs'; -import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; +import { mockEcsDataWithAlert } from '../../../common/mock/mock_ecs'; +import { sendAlertToTimelineAction, updateAlertStatusAction } from './actions'; import * as i18n from './translations'; jest.mock('./actions'); -describe('signals default_config', () => { - describe('buildSignalsRuleIdFilter', () => { +describe('alerts default_config', () => { + describe('buildAlertsRuleIdFilter', () => { test('given a rule id this will return an array with a single filter', () => { - const filters: Filter[] = buildSignalsRuleIdFilter('rule-id-1'); + const filters: Filter[] = buildAlertsRuleIdFilter('rule-id-1'); const expectedFilter: Filter = { meta: { alias: null, @@ -48,7 +48,7 @@ describe('signals default_config', () => { }); }); - describe('getSignalsActions', () => { + describe('getAlertActions', () => { let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; let setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; let createTimeline: CreateTimeline; @@ -67,8 +67,8 @@ describe('signals default_config', () => { }); describe('timeline tooltip', () => { - test('it invokes sendSignalToTimelineAction when button clicked', () => { - const signalsActions = getSignalsActions({ + test('it invokes sendAlertToTimelineAction when button clicked', () => { + const alertsActions = getAlertActions({ canUserCRUD: true, hasIndexWrite: true, setEventsLoading, @@ -79,24 +79,24 @@ describe('signals default_config', () => { onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, }); - const timelineAction = signalsActions[0].getAction({ + const timelineAction = alertsActions[0].getAction({ eventId: 'even-id', - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, }); const wrapper = mount(timelineAction as React.ReactElement); wrapper.find(EuiButtonIcon).simulate('click'); - expect(sendSignalToTimelineAction).toHaveBeenCalled(); + expect(sendAlertToTimelineAction).toHaveBeenCalled(); }); }); - describe('signal open action', () => { - let signalsActions: TimelineAction[]; - let signalOpenAction: JSX.Element; + describe('alert open action', () => { + let alertsActions: TimelineAction[]; + let alertOpenAction: JSX.Element; let wrapper: ReactWrapper; beforeEach(() => { - signalsActions = getSignalsActions({ + alertsActions = getAlertActions({ canUserCRUD: true, hasIndexWrite: true, setEventsLoading, @@ -108,23 +108,23 @@ describe('signals default_config', () => { onAlertStatusUpdateFailure, }); - signalOpenAction = signalsActions[1].getAction({ + alertOpenAction = alertsActions[1].getAction({ eventId: 'event-id', - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, }); - wrapper = mount(signalOpenAction as React.ReactElement); + wrapper = mount(alertOpenAction as React.ReactElement); }); afterEach(() => { wrapper.unmount(); }); - test('it invokes updateSignalStatusAction when button clicked', () => { + test('it invokes updateAlertStatusAction when button clicked', () => { wrapper.find(EuiButtonIcon).simulate('click'); - expect(updateSignalStatusAction).toHaveBeenCalledWith({ - signalIds: ['event-id'], + expect(updateAlertStatusAction).toHaveBeenCalledWith({ + alertIds: ['event-id'], status: 'open', setEventsLoading, setEventsDeleted, @@ -134,27 +134,27 @@ describe('signals default_config', () => { }); test('it displays expected text on hover', () => { - const openSignal = wrapper.find(EuiToolTip); - openSignal.simulate('mouseOver'); + const openAlert = wrapper.find(EuiToolTip); + openAlert.simulate('mouseOver'); const tooltip = wrapper.find('.euiToolTipPopover').text(); - expect(tooltip).toEqual(i18n.ACTION_OPEN_SIGNAL); + expect(tooltip).toEqual(i18n.ACTION_OPEN_ALERT); }); test('it displays expected icon', () => { const icon = wrapper.find(EuiButtonIcon).props().iconType; - expect(icon).toEqual('securitySignalDetected'); + expect(icon).toEqual('securityAlertDetected'); }); }); - describe('signal close action', () => { - let signalsActions: TimelineAction[]; - let signalCloseAction: JSX.Element; + describe('alert close action', () => { + let alertsActions: TimelineAction[]; + let alertCloseAction: JSX.Element; let wrapper: ReactWrapper; beforeEach(() => { - signalsActions = getSignalsActions({ + alertsActions = getAlertActions({ canUserCRUD: true, hasIndexWrite: true, setEventsLoading, @@ -166,23 +166,23 @@ describe('signals default_config', () => { onAlertStatusUpdateFailure, }); - signalCloseAction = signalsActions[1].getAction({ + alertCloseAction = alertsActions[1].getAction({ eventId: 'event-id', - ecsData: mockEcsDataWithSignal, + ecsData: mockEcsDataWithAlert, }); - wrapper = mount(signalCloseAction as React.ReactElement); + wrapper = mount(alertCloseAction as React.ReactElement); }); afterEach(() => { wrapper.unmount(); }); - test('it invokes updateSignalStatusAction when status button clicked', () => { + test('it invokes updateAlertStatusAction when status button clicked', () => { wrapper.find(EuiButtonIcon).simulate('click'); - expect(updateSignalStatusAction).toHaveBeenCalledWith({ - signalIds: ['event-id'], + expect(updateAlertStatusAction).toHaveBeenCalledWith({ + alertIds: ['event-id'], status: 'closed', setEventsLoading, setEventsDeleted, @@ -192,16 +192,16 @@ describe('signals default_config', () => { }); test('it displays expected text on hover', () => { - const closeSignal = wrapper.find(EuiToolTip); - closeSignal.simulate('mouseOver'); + const closeAlert = wrapper.find(EuiToolTip); + closeAlert.simulate('mouseOver'); const tooltip = wrapper.find('.euiToolTipPopover').text(); - expect(tooltip).toEqual(i18n.ACTION_CLOSE_SIGNAL); + expect(tooltip).toEqual(i18n.ACTION_CLOSE_ALERT); }); test('it displays expected icon', () => { const icon = wrapper.find(EuiButtonIcon).props().iconType; - expect(icon).toEqual('securitySignalResolved'); + expect(icon).toEqual('securityAlertResolved'); }); }); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx similarity index 81% rename from x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx index 1269f31064e9e..6cef2e7c53c46 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/default_config.tsx @@ -23,8 +23,8 @@ import { import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { FILTER_OPEN } from './signals_filter_group'; -import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; +import { FILTER_OPEN } from './alerts_filter_group'; +import { sendAlertToTimelineAction, updateAlertStatusAction } from './actions'; import * as i18n from './translations'; import { CreateTimeline, @@ -33,7 +33,7 @@ import { UpdateTimelineLoading, } from './types'; -export const signalsOpenFilters: Filter[] = [ +export const alertsOpenFilters: Filter[] = [ { meta: { alias: null, @@ -53,7 +53,7 @@ export const signalsOpenFilters: Filter[] = [ }, ]; -export const signalsClosedFilters: Filter[] = [ +export const alertsClosedFilters: Filter[] = [ { meta: { alias: null, @@ -73,7 +73,7 @@ export const signalsClosedFilters: Filter[] = [ }, ]; -export const buildSignalsRuleIdFilter = (ruleId: string): Filter[] => [ +export const buildAlertsRuleIdFilter = (ruleId: string): Filter[] => [ { meta: { alias: null, @@ -93,7 +93,7 @@ export const buildSignalsRuleIdFilter = (ruleId: string): Filter[] => [ }, ]; -export const signalsHeaders: ColumnHeaderOptions[] = [ +export const alertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', @@ -102,32 +102,32 @@ export const signalsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.name', - label: i18n.SIGNALS_HEADERS_RULE, + label: i18n.ALERTS_HEADERS_RULE, linkField: 'signal.rule.id', width: DEFAULT_COLUMN_MIN_WIDTH, }, { columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.version', - label: i18n.SIGNALS_HEADERS_VERSION, + label: i18n.ALERTS_HEADERS_VERSION, width: 100, }, { columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.type', - label: i18n.SIGNALS_HEADERS_METHOD, + label: i18n.ALERTS_HEADERS_METHOD, width: 100, }, { columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.severity', - label: i18n.SIGNALS_HEADERS_SEVERITY, + label: i18n.ALERTS_HEADERS_SEVERITY, width: 105, }, { columnHeaderType: defaultColumnHeaderType, id: 'signal.rule.risk_score', - label: i18n.SIGNALS_HEADERS_RISK_SCORE, + label: i18n.ALERTS_HEADERS_RISK_SCORE, width: 115, }, { @@ -171,9 +171,9 @@ export const signalsHeaders: ColumnHeaderOptions[] = [ }, ]; -export const signalsDefaultModel: SubsetTimelineModel = { +export const alertsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, - columns: signalsHeaders, + columns: alertsHeaders, showCheckboxes: true, showRowRenderers: false, }; @@ -189,7 +189,7 @@ export const requiredFieldsForActions = [ 'signal.rule.id', ]; -export const getSignalsActions = ({ +export const getAlertActions = ({ apolloClient, canUserCRUD, hasIndexWrite, @@ -215,13 +215,13 @@ export const getSignalsActions = ({ { getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( - sendSignalToTimelineAction({ + sendAlertToTimelineAction({ apolloClient, createTimeline, ecsData, @@ -233,20 +233,20 @@ export const getSignalsActions = ({ /> ), - id: 'sendSignalToTimeline', + id: 'sendAlertToTimeline', width: 26, }, { getAction: ({ eventId }: TimelineActionProps): JSX.Element => ( - updateSignalStatusAction({ - signalIds: [eventId], + updateAlertStatusAction({ + alertIds: [eventId], status, setEventsLoading, setEventsDeleted, @@ -255,12 +255,12 @@ export const getSignalsActions = ({ }) } isDisabled={!canUserCRUD || !hasIndexWrite} - iconType={status === FILTER_OPEN ? 'securitySignalDetected' : 'securitySignalResolved'} + iconType={status === FILTER_OPEN ? 'securityAlertDetected' : 'securityAlertResolved'} aria-label="Next" /> ), - id: 'updateSignalStatus', + id: 'updateAlertStatus', width: 26, }, ]; diff --git a/x-pack/plugins/siem/public/alerts/components/signals/helpers.test.ts b/x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/alerts/components/signals/helpers.test.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.test.ts diff --git a/x-pack/plugins/siem/public/alerts/components/signals/helpers.ts b/x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.ts similarity index 98% rename from x-pack/plugins/siem/public/alerts/components/signals/helpers.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.ts index e9b8fdda84053..11a03b0426891 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/helpers.ts +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/helpers.ts @@ -19,7 +19,7 @@ interface FindValueToChangeInQuery { /** * Fields that will be replaced with the template strings from a a saved timeline template. - * This is used for the signals detection engine feature when you save a timeline template + * This is used for the alerts detection engine feature when you save a timeline template * and are the fields you can replace when creating a template. */ const templateFields = [ diff --git a/x-pack/plugins/siem/public/alerts/components/signals/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/index.test.tsx similarity index 85% rename from x-pack/plugins/siem/public/alerts/components/signals/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/index.test.tsx index b66a9fc881045..51fdd828bcddb 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/index.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SignalsTableComponent } from './index'; +import { AlertsTableComponent } from './index'; -describe('SignalsTableComponent', () => { +describe('AlertsTableComponent', () => { it('renders correctly', () => { const wrapper = shallow( - { /> ); - expect(wrapper.find('[title="Signals"]')).toBeTruthy(); + expect(wrapper.find('[title="Alerts"]')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx b/x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx similarity index 81% rename from x-pack/plugins/siem/public/alerts/components/signals/index.tsx rename to x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx index effb6a2450dc2..05c811d8e19bd 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/index.tsx @@ -22,28 +22,28 @@ import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { useApolloClient } from '../../../common/utils/apollo_context'; -import { updateSignalStatusAction } from './actions'; +import { updateAlertStatusAction } from './actions'; import { - getSignalsActions, + getAlertActions, requiredFieldsForActions, - signalsClosedFilters, - signalsDefaultModel, - signalsOpenFilters, + alertsClosedFilters, + alertsDefaultModel, + alertsOpenFilters, } from './default_config'; import { FILTER_CLOSED, FILTER_OPEN, - SignalFilterOption, - SignalsTableFilterGroup, -} from './signals_filter_group'; -import { SignalsUtilityBar } from './signals_utility_bar'; + AlertFilterOption, + AlertsTableFilterGroup, +} from './alerts_filter_group'; +import { AlertsUtilityBar } from './alerts_utility_bar'; import * as i18n from './translations'; import { CreateTimelineProps, SetEventsDeletedProps, SetEventsLoadingProps, - UpdateSignalsStatusCallback, - UpdateSignalsStatusProps, + UpdateAlertsStatusCallback, + UpdateAlertsStatusProps, } from './types'; import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; import { @@ -52,7 +52,7 @@ import { displayErrorToast, } from '../../../common/components/toasters'; -export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; +export const ALERTS_TABLE_TIMELINE_ID = 'alerts-table'; interface OwnProps { canUserCRUD: boolean; @@ -64,9 +64,9 @@ interface OwnProps { to: number; } -type SignalsTableComponentProps = OwnProps & PropsFromRedux; +type AlertsTableComponentProps = OwnProps & PropsFromRedux; -export const SignalsTableComponent: React.FC = ({ +export const AlertsTableComponent: React.FC = ({ canUserCRUD, clearEventsDeleted, clearEventsLoading, @@ -91,7 +91,7 @@ export const SignalsTableComponent: React.FC = ({ const apolloClient = useApolloClient(); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( signalsIndex !== '' ? [signalsIndex] : [] ); @@ -140,16 +140,16 @@ export const SignalsTableComponent: React.FC = ({ const setEventsLoadingCallback = useCallback( ({ eventIds, isLoading }: SetEventsLoadingProps) => { - setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); + setEventsLoading!({ id: ALERTS_TABLE_TIMELINE_ID, eventIds, isLoading }); }, - [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] + [setEventsLoading, ALERTS_TABLE_TIMELINE_ID] ); const setEventsDeletedCallback = useCallback( ({ eventIds, isDeleted }: SetEventsDeletedProps) => { - setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); + setEventsDeleted!({ id: ALERTS_TABLE_TIMELINE_ID, eventIds, isDeleted }); }, - [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] + [setEventsDeleted, ALERTS_TABLE_TIMELINE_ID] ); const onAlertStatusUpdateSuccess = useCallback( @@ -184,10 +184,10 @@ export const SignalsTableComponent: React.FC = ({ // Callback for when open/closed filter changes const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: SignalFilterOption) => { - clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + (newFilterGroup: AlertFilterOption) => { + clearEventsLoading!({ id: ALERTS_TABLE_TIMELINE_ID }); + clearEventsDeleted!({ id: ALERTS_TABLE_TIMELINE_ID }); + clearSelected!({ id: ALERTS_TABLE_TIMELINE_ID }); setFilterGroup(newFilterGroup); }, [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] @@ -195,7 +195,7 @@ export const SignalsTableComponent: React.FC = ({ // Callback for clearing entire selection from utility bar const clearSelectionCallback = useCallback(() => { - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearSelected!({ id: ALERTS_TABLE_TIMELINE_ID }); setSelectAll(false); setShowClearSelectionAction(false); }, [clearSelected, setSelectAll, setShowClearSelectionAction]); @@ -208,11 +208,11 @@ export const SignalsTableComponent: React.FC = ({ setShowClearSelectionAction(true); }, [setSelectAll, setShowClearSelectionAction]); - const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( - async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { - await updateSignalStatusAction({ + const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback( + async (refetchQuery: inputsModel.Refetch, { alertIds, status }: UpdateAlertsStatusProps) => { + await updateAlertStatusAction({ query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, - signalIds: Object.keys(selectedEventIds), + alertIds: Object.keys(selectedEventIds), status, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, @@ -232,11 +232,11 @@ export const SignalsTableComponent: React.FC = ({ ] ); - // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component + // Callback for creating the AlertsUtilityBar which receives totalCount from EventsViewer component const utilityBarCallback = useCallback( (refetchQuery: inputsModel.Refetch, totalCount: number) => { return ( - 0} clearSelection={clearSelectionCallback} @@ -246,7 +246,7 @@ export const SignalsTableComponent: React.FC = ({ selectedEventIds={selectedEventIds} showClearSelection={showClearSelectionAction} totalCount={totalCount} - updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} + updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} /> ); }, @@ -259,14 +259,14 @@ export const SignalsTableComponent: React.FC = ({ selectAllCallback, selectedEventIds, showClearSelectionAction, - updateSignalsStatusCallback, + updateAlertsStatusCallback, ] ); - // Send to Timeline / Update Signal Status Actions for each table row + // Send to Timeline / Update Alert Status Actions for each table row const additionalActions = useMemo( () => - getSignalsActions({ + getAlertActions({ apolloClient, canUserCRUD, hasIndexWrite, @@ -295,38 +295,38 @@ export const SignalsTableComponent: React.FC = ({ const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo(() => { if (isEmpty(defaultFilters)) { - return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; + return filterGroup === FILTER_OPEN ? alertsOpenFilters : alertsClosedFilters; } else if (defaultFilters != null && !isEmpty(defaultFilters)) { return [ ...defaultFilters, - ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), + ...(filterGroup === FILTER_OPEN ? alertsOpenFilters : alertsClosedFilters), ]; } }, [defaultFilters, filterGroup]); const timelineTypeContext = useMemo( () => ({ - documentType: i18n.SIGNALS_DOCUMENT_TYPE, - footerText: i18n.TOTAL_COUNT_OF_SIGNALS, - loadingText: i18n.LOADING_SIGNALS, + documentType: i18n.ALERTS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_ALERTS, + loadingText: i18n.LOADING_ALERTS, queryFields: requiredFieldsForActions, timelineActions: additionalActions, - title: i18n.SIGNALS_TABLE_TITLE, + title: i18n.ALERTS_TABLE_TITLE, selectAll: canUserCRUD ? selectAll : false, }), [additionalActions, canUserCRUD, selectAll] ); const headerFilterGroup = useMemo( - () => , + () => , [onFilterGroupChangedCallback] ); if (loading || isEmpty(signalsIndex)) { return ( - - + + ); } @@ -335,10 +335,10 @@ export const SignalsTableComponent: React.FC = ({ { const getGlobalInputs = inputsSelectors.globalSelector(); const mapStateToProps = (state: State) => { const timeline: TimelineModel = - getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; + getTimeline(state, ALERTS_TABLE_TIMELINE_ID) ?? timelineDefaults; const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); @@ -401,4 +401,4 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; -export const SignalsTable = connector(React.memo(SignalsTableComponent)); +export const AlertsTable = connector(React.memo(AlertsTableComponent)); diff --git a/x-pack/plugins/siem/public/alerts/components/alerts_table/translations.ts b/x-pack/plugins/siem/public/alerts/components/alerts_table/translations.ts new file mode 100644 index 0000000000000..4f34e9d031eed --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/translations.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', { + defaultMessage: 'Detection engine', +}); + +export const ALERTS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.alerts.tableTitle', { + defaultMessage: 'Alert list', +}); + +export const ALERTS_DOCUMENT_TYPE = i18n.translate( + 'xpack.siem.detectionEngine.alerts.documentTypeTitle', + { + defaultMessage: 'Alerts', + } +); + +export const OPEN_ALERTS = i18n.translate('xpack.siem.detectionEngine.alerts.openAlertsTitle', { + defaultMessage: 'Open alerts', +}); + +export const CLOSED_ALERTS = i18n.translate('xpack.siem.detectionEngine.alerts.closedAlertsTitle', { + defaultMessage: 'Closed alerts', +}); + +export const LOADING_ALERTS = i18n.translate( + 'xpack.siem.detectionEngine.alerts.loadingAlertsTitle', + { + defaultMessage: 'Loading Alerts', + } +); + +export const TOTAL_COUNT_OF_ALERTS = i18n.translate( + 'xpack.siem.detectionEngine.alerts.totalCountOfAlertsTitle', + { + defaultMessage: 'alerts match the search criteria', + } +); + +export const ALERTS_HEADERS_RULE = i18n.translate( + 'xpack.siem.eventsViewer.alerts.defaultHeaders.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const ALERTS_HEADERS_VERSION = i18n.translate( + 'xpack.siem.eventsViewer.alerts.defaultHeaders.versionTitle', + { + defaultMessage: 'Version', + } +); + +export const ALERTS_HEADERS_METHOD = i18n.translate( + 'xpack.siem.eventsViewer.alerts.defaultHeaders.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const ALERTS_HEADERS_SEVERITY = i18n.translate( + 'xpack.siem.eventsViewer.alerts.defaultHeaders.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const ALERTS_HEADERS_RISK_SCORE = i18n.translate( + 'xpack.siem.eventsViewer.alerts.defaultHeaders.riskScoreTitle', + { + defaultMessage: 'Risk Score', + } +); + +export const ACTION_OPEN_ALERT = i18n.translate( + 'xpack.siem.detectionEngine.alerts.actions.openAlertTitle', + { + defaultMessage: 'Open alert', + } +); + +export const ACTION_CLOSE_ALERT = i18n.translate( + 'xpack.siem.detectionEngine.alerts.actions.closeAlertTitle', + { + defaultMessage: 'Close alert', + } +); + +export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( + 'xpack.siem.detectionEngine.alerts.actions.investigateInTimelineTitle', + { + defaultMessage: 'Investigate in timeline', + } +); + +export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.alerts.closedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => + i18n.translate('xpack.siem.detectionEngine.alerts.openedAlertSuccessToastMessage', { + values: { totalAlerts }, + defaultMessage: + 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', + }); + +export const CLOSED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.siem.detectionEngine.alerts.closedAlertFailedToastMessage', + { + defaultMessage: 'Failed to close alert(s).', + } +); + +export const OPENED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.siem.detectionEngine.alerts.openedAlertFailedToastMessage', + { + defaultMessage: 'Failed to open alert(s)', + } +); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/types.ts b/x-pack/plugins/siem/public/alerts/components/alerts_table/types.ts similarity index 77% rename from x-pack/plugins/siem/public/alerts/components/signals/types.ts rename to x-pack/plugins/siem/public/alerts/components/alerts_table/types.ts index 542aa61074ce1..ba342ae441857 100644 --- a/x-pack/plugins/siem/public/alerts/components/signals/types.ts +++ b/x-pack/plugins/siem/public/alerts/components/alerts_table/types.ts @@ -20,20 +20,20 @@ export interface SetEventsDeletedProps { isDeleted: boolean; } -export interface UpdateSignalsStatusProps { - signalIds: string[]; +export interface UpdateAlertsStatusProps { + alertIds: string[]; status: 'open' | 'closed'; } -export type UpdateSignalsStatusCallback = ( +export type UpdateAlertsStatusCallback = ( refetchQuery: inputsModel.Refetch, - { signalIds, status }: UpdateSignalsStatusProps + { alertIds, status }: UpdateAlertsStatusProps ) => void; -export type UpdateSignalsStatus = ({ signalIds, status }: UpdateSignalsStatusProps) => void; +export type UpdateAlertsStatus = ({ alertIds, status }: UpdateAlertsStatusProps) => void; -export interface UpdateSignalStatusActionProps { +export interface UpdateAlertStatusActionProps { query?: string; - signalIds: string[]; + alertIds: string[]; status: 'open' | 'closed'; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; @@ -41,9 +41,7 @@ export interface UpdateSignalStatusActionProps { onAlertStatusUpdateFailure: (status: string, error: Error) => void; } -export type SendSignalsToTimeline = () => void; - -export interface SendSignalToTimelineActionProps { +export interface SendAlertToTimelineActionProps { apolloClient?: ApolloClient<{}>; createTimeline: CreateTimeline; ecsData: Ecs; diff --git a/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/translations.ts b/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/translations.ts index 303431a559e8f..651faf0b17318 100644 --- a/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/translations.ts +++ b/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/translations.ts @@ -17,6 +17,6 @@ export const PAGE_BADGE_TOOLTIP = i18n.translate( 'xpack.siem.detectionEngine.headerPage.pageBadgeTooltip', { defaultMessage: - 'Detections is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', + 'Alerts is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', } ); diff --git a/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.test.tsx similarity index 72% rename from x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.test.tsx index 2e6890e60fc61..2b1173f8a4843 100644 --- a/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.test.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { NoWriteSignalsCallOut } from './index'; +import { NoWriteAlertsCallOut } from './index'; -describe('no_write_signals_callout', () => { +describe('no_write_alerts_callout', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('EuiCallOut')).toBeTruthy(); }); diff --git a/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.tsx b/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.tsx similarity index 72% rename from x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.tsx rename to x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.tsx index 1950531998450..dcb50ef43a841 100644 --- a/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/index.tsx @@ -9,13 +9,13 @@ import React, { memo, useCallback, useState } from 'react'; import * as i18n from './translations'; -const NoWriteSignalsCallOutComponent = () => { +const NoWriteAlertsCallOutComponent = () => { const [showCallOut, setShowCallOut] = useState(true); const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); return showCallOut ? ( - -

    {i18n.NO_WRITE_SIGNALS_CALLOUT_MSG}

    + +

    {i18n.NO_WRITE_ALERTS_CALLOUT_MSG}

    {i18n.DISMISS_CALLOUT} @@ -23,4 +23,4 @@ const NoWriteSignalsCallOutComponent = () => { ) : null; }; -export const NoWriteSignalsCallOut = memo(NoWriteSignalsCallOutComponent); +export const NoWriteAlertsCallOut = memo(NoWriteAlertsCallOutComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/translations.ts b/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/translations.ts similarity index 52% rename from x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/translations.ts rename to x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/translations.ts index 065d775e1dc6a..f79ede56ef9ae 100644 --- a/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/translations.ts +++ b/x-pack/plugins/siem/public/alerts/components/no_write_alerts_callout/translations.ts @@ -6,23 +6,23 @@ import { i18n } from '@kbn/i18n'; -export const NO_WRITE_SIGNALS_CALLOUT_TITLE = i18n.translate( - 'xpack.siem.detectionEngine.noWriteSignalsCallOutTitle', +export const NO_WRITE_ALERTS_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.noWriteAlertsCallOutTitle', { - defaultMessage: 'Signals index permissions required', + defaultMessage: 'Alerts index permissions required', } ); -export const NO_WRITE_SIGNALS_CALLOUT_MSG = i18n.translate( - 'xpack.siem.detectionEngine.noWriteSignalsCallOutMsg', +export const NO_WRITE_ALERTS_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.noWriteAlertsCallOutMsg', { defaultMessage: - 'You are currently missing the required permissions to update signals. Please contact your administrator for further assistance.', + 'You are currently missing the required permissions to update alerts. Please contact your administrator for further assistance.', } ); export const DISMISS_CALLOUT = i18n.translate( - 'xpack.siem.detectionEngine.dismissNoWriteSignalButton', + 'xpack.siem.detectionEngine.dismissNoWriteAlertButton', { defaultMessage: 'Dismiss', } diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/translations.ts index 407dedbf27baf..9b36d96cef9ca 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/translations.ts @@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate( 'xpack.siem.detectionEngine.rules.prePackagedRules.emptyPromptMessage', { defaultMessage: - 'Elastic SIEM comes with prebuilt detection rules that run in the background and create signals when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + 'Elastic SIEM comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', } ); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx index 5823612faac13..b77de683d5f20 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx @@ -26,7 +26,7 @@ type ThrottleSelectField = typeof SelectField; const DEFAULT_ACTION_GROUP_ID = 'default'; const DEFAULT_ACTION_MESSAGE = - 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; + 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts'; const FieldErrorsContainer = styled.div` p { diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx index 33aa02adf3f10..69e8ed10d1b34 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx @@ -183,7 +183,7 @@ export const schema: FormSchema = { }), helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideHelpText', { defaultMessage: - 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from signals generated by this rule.', + 'Provide helpful information for analysts that are performing an alert investigation. This guide will appear on both the rule details page and in timelines created from alerts generated by this rule.', }), labelAppend: OptionalFieldLabel, }, diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx index 14afa63ef3609..0c309c8c51a15 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx @@ -168,7 +168,7 @@ export const schema: FormSchema = { helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', { - defaultMessage: 'Select which timeline to use when investigating generated signals.', + defaultMessage: 'Select which timeline to use when investigating generated alerts.', } ), }, diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx index 99ff8a6727372..d010a3128b24d 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx @@ -22,8 +22,7 @@ export const schema: FormSchema = { helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', { - defaultMessage: - 'Rules run periodically and detect signals within the specified time frame.', + defaultMessage: 'Rules run periodically and detect alerts within the specified time frame.', } ), }, @@ -38,7 +37,7 @@ export const schema: FormSchema = { helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', { - defaultMessage: 'Adds time to the look-back period to prevent missed signals.', + defaultMessage: 'Adds time to the look-back period to prevent missed alerts.', } ), }, diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/translations.ts deleted file mode 100644 index b876177d5e4d1..0000000000000 --- a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/translations.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const SHOWING_SIGNALS = (totalSignalsFormatted: string, totalSignals: number) => - i18n.translate('xpack.siem.detectionEngine.signals.utilityBar.showingSignalsTitle', { - values: { totalSignalsFormatted, totalSignals }, - defaultMessage: - 'Showing {totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}', - }); - -export const SELECTED_SIGNALS = (selectedSignalsFormatted: string, selectedSignals: number) => - i18n.translate('xpack.siem.detectionEngine.signals.utilityBar.selectedSignalsTitle', { - values: { selectedSignalsFormatted, selectedSignals }, - defaultMessage: - 'Selected {selectedSignalsFormatted} {selectedSignals, plural, =1 {signal} other {signals}}', - }); - -export const SELECT_ALL_SIGNALS = (totalSignalsFormatted: string, totalSignals: number) => - i18n.translate('xpack.siem.detectionEngine.signals.utilityBar.selectAllSignalsTitle', { - values: { totalSignalsFormatted, totalSignals }, - defaultMessage: - 'Select all {totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}', - }); - -export const CLEAR_SELECTION = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.clearSelectionTitle', - { - defaultMessage: 'Clear selection', - } -); - -export const BATCH_ACTIONS = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.batchActionsTitle', - { - defaultMessage: 'Batch actions', - } -); - -export const BATCH_ACTION_VIEW_SELECTED_IN_HOSTS = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInHostsTitle', - { - defaultMessage: 'View selected in hosts', - } -); - -export const BATCH_ACTION_VIEW_SELECTED_IN_NETWORK = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInNetworkTitle', - { - defaultMessage: 'View selected in network', - } -); - -export const BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInTimelineTitle', - { - defaultMessage: 'View selected in timeline', - } -); - -export const BATCH_ACTION_OPEN_SELECTED = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.batchActions.openSelectedTitle', - { - defaultMessage: 'Open selected', - } -); - -export const BATCH_ACTION_CLOSE_SELECTED = i18n.translate( - 'xpack.siem.detectionEngine.signals.utilityBar.batchActions.closeSelectedTitle', - { - defaultMessage: 'Close selected', - } -); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals/translations.ts deleted file mode 100644 index e49ff9846b9b7..0000000000000 --- a/x-pack/plugins/siem/public/alerts/components/signals/translations.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', { - defaultMessage: 'Detection engine', -}); - -export const SIGNALS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.signals.tableTitle', { - defaultMessage: 'Signals', -}); - -export const SIGNALS_DOCUMENT_TYPE = i18n.translate( - 'xpack.siem.detectionEngine.signals.documentTypeTitle', - { - defaultMessage: 'Signals', - } -); - -export const OPEN_SIGNALS = i18n.translate('xpack.siem.detectionEngine.signals.openSignalsTitle', { - defaultMessage: 'Open signals', -}); - -export const CLOSED_SIGNALS = i18n.translate( - 'xpack.siem.detectionEngine.signals.closedSignalsTitle', - { - defaultMessage: 'Closed signals', - } -); - -export const LOADING_SIGNALS = i18n.translate( - 'xpack.siem.detectionEngine.signals.loadingSignalsTitle', - { - defaultMessage: 'Loading Signals', - } -); - -export const TOTAL_COUNT_OF_SIGNALS = i18n.translate( - 'xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle', - { - defaultMessage: 'signals match the search criteria', - } -); - -export const SIGNALS_HEADERS_RULE = i18n.translate( - 'xpack.siem.eventsViewer.signals.defaultHeaders.ruleTitle', - { - defaultMessage: 'Rule', - } -); - -export const SIGNALS_HEADERS_VERSION = i18n.translate( - 'xpack.siem.eventsViewer.signals.defaultHeaders.versionTitle', - { - defaultMessage: 'Version', - } -); - -export const SIGNALS_HEADERS_METHOD = i18n.translate( - 'xpack.siem.eventsViewer.signals.defaultHeaders.methodTitle', - { - defaultMessage: 'Method', - } -); - -export const SIGNALS_HEADERS_SEVERITY = i18n.translate( - 'xpack.siem.eventsViewer.signals.defaultHeaders.severityTitle', - { - defaultMessage: 'Severity', - } -); - -export const SIGNALS_HEADERS_RISK_SCORE = i18n.translate( - 'xpack.siem.eventsViewer.signals.defaultHeaders.riskScoreTitle', - { - defaultMessage: 'Risk Score', - } -); - -export const ACTION_OPEN_SIGNAL = i18n.translate( - 'xpack.siem.detectionEngine.signals.actions.openSignalTitle', - { - defaultMessage: 'Open signal', - } -); - -export const ACTION_CLOSE_SIGNAL = i18n.translate( - 'xpack.siem.detectionEngine.signals.actions.closeSignalTitle', - { - defaultMessage: 'Close signal', - } -); - -export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( - 'xpack.siem.detectionEngine.signals.actions.investigateInTimelineTitle', - { - defaultMessage: 'Investigate in timeline', - } -); - -export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => - i18n.translate('xpack.siem.detectionEngine.signals.closedAlertSuccessToastMessage', { - values: { totalAlerts }, - defaultMessage: - 'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', - }); - -export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => - i18n.translate('xpack.siem.detectionEngine.signals.openedAlertSuccessToastMessage', { - values: { totalAlerts }, - defaultMessage: - 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', - }); - -export const CLOSED_ALERT_FAILED_TOAST = i18n.translate( - 'xpack.siem.detectionEngine.signals.closedAlertFailedToastMessage', - { - defaultMessage: 'Failed to close alert(s).', - } -); - -export const OPENED_ALERT_FAILED_TOAST = i18n.translate( - 'xpack.siem.detectionEngine.signals.openedAlertFailedToastMessage', - { - defaultMessage: 'Failed to open alert(s)', - } -); diff --git a/x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx deleted file mode 100644 index b1d7f2cfe7eb5..0000000000000 --- a/x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiLoadingSpinner } from '@elastic/eui'; -import { FormattedRelative } from '@kbn/i18n/react'; -import React, { useState, useEffect } from 'react'; - -import { useQuerySignals } from '../../containers/detection_engine/signals/use_query'; -import { buildLastSignalsQuery } from './query.dsl'; -import { Aggs } from './types'; - -interface SignalInfo { - ruleId?: string | null; -} - -type Return = [React.ReactNode, React.ReactNode]; - -export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { - const [lastSignals, setLastSignals] = useState( - - ); - const [totalSignals, setTotalSignals] = useState( - - ); - - const { loading, data: signals } = useQuerySignals(buildLastSignalsQuery(ruleId)); - - useEffect(() => { - if (signals != null) { - const mySignals = signals; - setLastSignals( - mySignals.aggregations?.lastSeen.value != null ? ( - - ) : null - ); - setTotalSignals(<>{mySignals.hits.total.value}); - } else { - setLastSignals(null); - setTotalSignals(null); - } - }, [loading, signals]); - - return [lastSignals, totalSignals]; -}; diff --git a/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx index 81b2c4347e17c..b01edac2605eb 100644 --- a/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx @@ -7,11 +7,11 @@ import { renderHook } from '@testing-library/react-hooks'; import { useUserInfo } from './index'; -import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; -import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; +import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user'; +import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; import { useKibana } from '../../../common/lib/kibana'; -jest.mock('../../containers/detection_engine/signals/use_privilege_user'); -jest.mock('../../containers/detection_engine/signals/use_signal_index'); +jest.mock('../../containers/detection_engine/alerts/use_privilege_user'); +jest.mock('../../containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../common/lib/kibana'); describe('useUserInfo', () => { diff --git a/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx b/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx index faf9016292559..049463d4066d8 100644 --- a/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx @@ -7,8 +7,8 @@ import { noop } from 'lodash/fp'; import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react'; -import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; -import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; +import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user'; +import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; import { useKibana } from '../../../common/lib/kibana'; export interface State { diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/__mocks__/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts similarity index 56% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/__mocks__/api.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts index 7cb1d7d574cf8..64a55a8ec6eba 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/__mocks__/api.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts @@ -4,26 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - QuerySignals, - SignalSearchResponse, - BasicSignals, - SignalsIndex, - Privilege, -} from '../types'; -import { signalsMock, mockSignalIndex, mockUserPrivilege } from '../mock'; +import { QueryAlerts, AlertSearchResponse, BasicSignals, AlertsIndex, Privilege } from '../types'; +import { alertsMock, mockSignalIndex, mockUserPrivilege } from '../mock'; -export const fetchQuerySignals = async ({ +export const fetchQueryAlerts = async ({ query, signal, -}: QuerySignals): Promise> => - Promise.resolve(signalsMock as SignalSearchResponse); +}: QueryAlerts): Promise> => + Promise.resolve(alertsMock as AlertSearchResponse); -export const getSignalIndex = async ({ signal }: BasicSignals): Promise => +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => Promise.resolve(mockSignalIndex); export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => Promise.resolve(mockUserPrivilege); -export const createSignalIndex = async ({ signal }: BasicSignals): Promise => +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => Promise.resolve(mockSignalIndex); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.test.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.test.ts similarity index 68% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.test.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.test.ts index 67d81d19faa7c..3cd819b55685c 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.test.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.test.ts @@ -6,15 +6,15 @@ import { KibanaServices } from '../../../../common/lib/kibana'; import { - signalsMock, - mockSignalsQuery, - mockStatusSignalQuery, + alertsMock, + mockAlertsQuery, + mockStatusAlertQuery, mockSignalIndex, mockUserPrivilege, } from './mock'; import { - fetchQuerySignals, - updateSignalStatus, + fetchQueryAlerts, + updateAlertStatus, getSignalIndex, getUserPrivilege, createSignalIndex, @@ -27,41 +27,41 @@ jest.mock('../../../../common/lib/kibana'); const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); -describe('Detections Signals API', () => { - describe('fetchQuerySignals', () => { +describe('Detections Alerts API', () => { + describe('fetchQueryAlerts', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(signalsMock); + fetchMock.mockResolvedValue(alertsMock); }); test('check parameter url, body', async () => { - await fetchQuerySignals({ query: mockSignalsQuery, signal: abortCtrl.signal }); + await fetchQueryAlerts({ query: mockAlertsQuery, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/search', { body: - '{"aggs":{"signalsByGrouping":{"terms":{"field":"signal.rule.risk_score","missing":"All others","order":{"_count":"desc"},"size":10},"aggs":{"signals":{"date_histogram":{"field":"@timestamp","fixed_interval":"81000000ms","min_doc_count":0,"extended_bounds":{"min":1579644343954,"max":1582236343955}}}}}},"query":{"bool":{"filter":[{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}},{"range":{"@timestamp":{"gte":1579644343954,"lte":1582236343955}}}]}}}', + '{"aggs":{"alertsByGrouping":{"terms":{"field":"signal.rule.risk_score","missing":"All others","order":{"_count":"desc"},"size":10},"aggs":{"alerts":{"date_histogram":{"field":"@timestamp","fixed_interval":"81000000ms","min_doc_count":0,"extended_bounds":{"min":1579644343954,"max":1582236343955}}}}}},"query":{"bool":{"filter":[{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}},{"range":{"@timestamp":{"gte":1579644343954,"lte":1582236343955}}}]}}}', method: 'POST', signal: abortCtrl.signal, }); }); test('happy path', async () => { - const signalsResp = await fetchQuerySignals({ - query: mockSignalsQuery, + const signalsResp = await fetchQueryAlerts({ + query: mockAlertsQuery, signal: abortCtrl.signal, }); - expect(signalsResp).toEqual(signalsMock); + expect(signalsResp).toEqual(alertsMock); }); }); - describe('updateSignalStatus', () => { + describe('updateAlertStatus', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue({}); }); - test('check parameter url, body when closing a signal', async () => { - await updateSignalStatus({ - query: mockStatusSignalQuery, + test('check parameter url, body when closing an alert', async () => { + await updateAlertStatus({ + query: mockStatusAlertQuery, signal: abortCtrl.signal, status: 'closed', }); @@ -73,9 +73,9 @@ describe('Detections Signals API', () => { }); }); - test('check parameter url, body when opening a signal', async () => { - await updateSignalStatus({ - query: mockStatusSignalQuery, + test('check parameter url, body when opening an alert', async () => { + await updateAlertStatus({ + query: mockStatusAlertQuery, signal: abortCtrl.signal, status: 'open', }); @@ -88,12 +88,12 @@ describe('Detections Signals API', () => { }); test('happy path', async () => { - const signalsResp = await updateSignalStatus({ - query: mockStatusSignalQuery, + const alertsResp = await updateAlertStatus({ + query: mockStatusAlertQuery, signal: abortCtrl.signal, status: 'open', }); - expect(signalsResp).toEqual({}); + expect(alertsResp).toEqual({}); }); }); @@ -112,10 +112,10 @@ describe('Detections Signals API', () => { }); test('happy path', async () => { - const signalsResp = await getSignalIndex({ + const alertsResp = await getSignalIndex({ signal: abortCtrl.signal, }); - expect(signalsResp).toEqual(mockSignalIndex); + expect(alertsResp).toEqual(mockSignalIndex); }); }); @@ -134,10 +134,10 @@ describe('Detections Signals API', () => { }); test('happy path', async () => { - const signalsResp = await getUserPrivilege({ + const alertsResp = await getUserPrivilege({ signal: abortCtrl.signal, }); - expect(signalsResp).toEqual(mockUserPrivilege); + expect(alertsResp).toEqual(mockUserPrivilege); }); }); @@ -156,10 +156,10 @@ describe('Detections Signals API', () => { }); test('happy path', async () => { - const signalsResp = await createSignalIndex({ + const alertsResp = await createSignalIndex({ signal: abortCtrl.signal, }); - expect(signalsResp).toEqual(mockSignalIndex); + expect(alertsResp).toEqual(mockSignalIndex); }); }); }); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.ts similarity index 72% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.ts index 445f66457c59e..ccf35c9671836 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReindexResponse } from 'elasticsearch'; +import { UpdateDocumentByQueryResponse } from 'elasticsearch'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -15,25 +15,25 @@ import { KibanaServices } from '../../../../common/lib/kibana'; import { BasicSignals, Privilege, - QuerySignals, - SignalSearchResponse, - SignalsIndex, - UpdateSignalStatusProps, + QueryAlerts, + AlertSearchResponse, + AlertsIndex, + UpdateAlertStatusProps, } from './types'; /** - * Fetch Signals by providing a query + * Fetch Alerts by providing a query * * @param query String to match a dsl * @param signal to cancel request * * @throws An error if response is not OK */ -export const fetchQuerySignals = async ({ +export const fetchQueryAlerts = async ({ query, signal, -}: QuerySignals): Promise> => - KibanaServices.get().http.fetch>( +}: QueryAlerts): Promise> => + KibanaServices.get().http.fetch>( DETECTION_ENGINE_QUERY_SIGNALS_URL, { method: 'POST', @@ -43,19 +43,19 @@ export const fetchQuerySignals = async ({ ); /** - * Update signal status by query + * Update alert status by query * - * @param query of signals to update + * @param query of alerts to update * @param status to update to('open' / 'closed') * @param signal AbortSignal for cancelling request * * @throws An error if response is not OK */ -export const updateSignalStatus = async ({ +export const updateAlertStatus = async ({ query, status, signal, -}: UpdateSignalStatusProps): Promise => +}: UpdateAlertStatusProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, ...query }), @@ -69,8 +69,8 @@ export const updateSignalStatus = async ({ * * @throws An error if response is not OK */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { method: 'GET', signal, }); @@ -95,8 +95,8 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { method: 'POST', signal, }); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/mock.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/mock.ts similarity index 98% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/mock.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/mock.ts index 6b0c7e0078268..cd2cc1fe390ba 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/mock.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSearchResponse, SignalsIndex, Privilege } from './types'; +import { AlertSearchResponse, AlertsIndex, Privilege } from './types'; -export const signalsMock: SignalSearchResponse = { +export const alertsMock: AlertSearchResponse = { took: 7, timeout: false, _shards: { @@ -902,14 +902,14 @@ export const signalsMock: SignalSearchResponse = { ], }, aggregations: { - signalsByGrouping: { + alertsByGrouping: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: '4', doc_count: 12600, - signals: { + alerts: { buckets: [ { key_as_string: '2020-01-21T04:30:00.000Z', @@ -939,9 +939,9 @@ export const signalsMock: SignalSearchResponse = { }, }; -export const mockSignalsQuery: object = { +export const mockAlertsQuery: object = { aggs: { - signalsByGrouping: { + alertsByGrouping: { terms: { field: 'signal.rule.risk_score', missing: 'All others', @@ -949,7 +949,7 @@ export const mockSignalsQuery: object = { size: 10, }, aggs: { - signals: { + alerts: { date_histogram: { field: '@timestamp', fixed_interval: '81000000ms', @@ -970,7 +970,7 @@ export const mockSignalsQuery: object = { }, }; -export const mockStatusSignalQuery: object = { +export const mockStatusAlertQuery: object = { bool: { filter: { terms: { _id: ['b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5'] }, @@ -978,7 +978,7 @@ export const mockStatusSignalQuery: object = { }, }; -export const mockSignalIndex: SignalsIndex = { +export const mockSignalIndex: AlertsIndex = { name: 'mock-signal-index', }; diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/translations.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/translations.ts similarity index 55% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/translations.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/translations.ts index 2b8f54e5438df..2f3ebccdb14cd 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/translations.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/translations.ts @@ -6,29 +6,29 @@ import { i18n } from '@kbn/i18n'; -export const SIGNAL_FETCH_FAILURE = i18n.translate( - 'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription', +export const ALERT_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.alerts.errorFetchingAlertsDescription', { - defaultMessage: 'Failed to query signals', + defaultMessage: 'Failed to query alerts', } ); export const PRIVILEGE_FETCH_FAILURE = i18n.translate( - 'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription', + 'xpack.siem.containers.detectionEngine.alerts.errorFetchingAlertsDescription', { - defaultMessage: 'Failed to query signals', + defaultMessage: 'Failed to query alerts', } ); export const SIGNAL_GET_NAME_FAILURE = i18n.translate( - 'xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription', + 'xpack.siem.containers.detectionEngine.alerts.errorGetAlertDescription', { defaultMessage: 'Failed to get signal index name', } ); export const SIGNAL_POST_FAILURE = i18n.translate( - 'xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription', + 'xpack.siem.containers.detectionEngine.alerts.errorPostAlertDescription', { defaultMessage: 'Failed to create signal index', } diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/types.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/types.ts similarity index 88% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/types.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/types.ts index 4e97c597546a7..b425cfd54a7fd 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/types.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/types.ts @@ -7,17 +7,17 @@ export interface BasicSignals { signal: AbortSignal; } -export interface QuerySignals extends BasicSignals { +export interface QueryAlerts extends BasicSignals { query: object; } -export interface SignalsResponse { +export interface AlertsResponse { took: number; timeout: boolean; } -export interface SignalSearchResponse - extends SignalsResponse { +export interface AlertSearchResponse + extends AlertsResponse { _shards: { total: number; successful: number; @@ -34,13 +34,13 @@ export interface SignalSearchResponse }; } -export interface UpdateSignalStatusProps { +export interface UpdateAlertStatusProps { query: object; status: 'open' | 'closed'; signal?: AbortSignal; // TODO: implement cancelling } -export interface SignalsIndex { +export interface AlertsIndex { name: string; } diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.test.tsx similarity index 61% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.test.tsx index c577f291f037e..8627b953c8dac 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.test.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.test.tsx @@ -5,13 +5,13 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { useQuerySignals, ReturnQuerySignals } from './use_query'; +import { useQueryAlerts, ReturnQueryAlerts } from './use_query'; import * as api from './api'; -import { mockSignalsQuery, signalsMock } from './mock'; +import { mockAlertsQuery, alertsMock } from './mock'; jest.mock('./api'); -describe('useQuerySignals', () => { +describe('useQueryAlerts', () => { const indexName = 'mock-index-name'; beforeEach(() => { jest.resetAllMocks(); @@ -20,8 +20,8 @@ describe('useQuerySignals', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook< [object, string], - ReturnQuerySignals - >(() => useQuerySignals(mockSignalsQuery, indexName)); + ReturnQueryAlerts + >(() => useQueryAlerts(mockAlertsQuery, indexName)); await waitForNextUpdate(); expect(result.current).toEqual({ loading: true, @@ -34,72 +34,72 @@ describe('useQuerySignals', () => { }); }); - test('fetch signals data', async () => { + test('fetch alerts data', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook< [object, string], - ReturnQuerySignals - >(() => useQuerySignals(mockSignalsQuery, indexName)); + ReturnQueryAlerts + >(() => useQueryAlerts(mockAlertsQuery, indexName)); await waitForNextUpdate(); await waitForNextUpdate(); expect(result.current).toEqual({ loading: false, - data: signalsMock, - response: JSON.stringify(signalsMock, null, 2), - request: JSON.stringify({ index: [indexName] ?? [''], body: mockSignalsQuery }, null, 2), + data: alertsMock, + response: JSON.stringify(alertsMock, null, 2), + request: JSON.stringify({ index: [indexName] ?? [''], body: mockAlertsQuery }, null, 2), setQuery: result.current.setQuery, refetch: result.current.refetch, }); }); }); - test('re-fetch signals data', async () => { - const spyOnfetchQuerySignals = jest.spyOn(api, 'fetchQuerySignals'); + test('re-fetch alerts data', async () => { + const spyOnfetchQueryAlerts = jest.spyOn(api, 'fetchQueryAlerts'); await act(async () => { const { result, waitForNextUpdate } = renderHook< [object, string], - ReturnQuerySignals - >(() => useQuerySignals(mockSignalsQuery, indexName)); + ReturnQueryAlerts + >(() => useQueryAlerts(mockAlertsQuery, indexName)); await waitForNextUpdate(); await waitForNextUpdate(); if (result.current.refetch) { result.current.refetch(); } await waitForNextUpdate(); - expect(spyOnfetchQuerySignals).toHaveBeenCalledTimes(2); + expect(spyOnfetchQueryAlerts).toHaveBeenCalledTimes(2); }); }); - test('fetch signal when index name changed', async () => { - const spyOnfetchRules = jest.spyOn(api, 'fetchQuerySignals'); + test('fetch alert when index name changed', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchQueryAlerts'); await act(async () => { const { rerender, waitForNextUpdate } = renderHook< [object, string], - ReturnQuerySignals - >((args) => useQuerySignals(args[0], args[1]), { - initialProps: [mockSignalsQuery, indexName], + ReturnQueryAlerts + >((args) => useQueryAlerts(args[0], args[1]), { + initialProps: [mockAlertsQuery, indexName], }); await waitForNextUpdate(); await waitForNextUpdate(); - rerender([mockSignalsQuery, 'new-mock-index-name']); + rerender([mockAlertsQuery, 'new-mock-index-name']); await waitForNextUpdate(); expect(spyOnfetchRules).toHaveBeenCalledTimes(2); }); }); - test('fetch signal when query object changed', async () => { - const spyOnfetchRules = jest.spyOn(api, 'fetchQuerySignals'); + test('fetch alert when query object changed', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchQueryAlerts'); await act(async () => { const { result, waitForNextUpdate } = renderHook< [object, string], - ReturnQuerySignals - >((args) => useQuerySignals(args[0], args[1]), { - initialProps: [mockSignalsQuery, indexName], + ReturnQueryAlerts + >((args) => useQueryAlerts(args[0], args[1]), { + initialProps: [mockAlertsQuery, indexName], }); await waitForNextUpdate(); await waitForNextUpdate(); if (result.current.setQuery) { - result.current.setQuery({ ...mockSignalsQuery }); + result.current.setQuery({ ...mockAlertsQuery }); } await waitForNextUpdate(); expect(spyOnfetchRules).toHaveBeenCalledTimes(2); @@ -107,13 +107,13 @@ describe('useQuerySignals', () => { }); test('if there is an error when fetching data, we should get back the init value for every properties', async () => { - const spyOnGetUserPrivilege = jest.spyOn(api, 'fetchQuerySignals'); + const spyOnGetUserPrivilege = jest.spyOn(api, 'fetchQueryAlerts'); spyOnGetUserPrivilege.mockImplementation(() => { throw new Error('Something went wrong, let see what happen'); }); await act(async () => { - const { result, waitForNextUpdate } = renderHook>( - () => useQuerySignals(mockSignalsQuery, 'mock-index-name') + const { result, waitForNextUpdate } = renderHook>( + () => useQueryAlerts(mockAlertsQuery, 'mock-index-name') ); await waitForNextUpdate(); await waitForNextUpdate(); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.tsx similarity index 69% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.tsx index 531e080ed7d1f..9c992fa872705 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_query.tsx @@ -6,14 +6,14 @@ import React, { SetStateAction, useEffect, useState } from 'react'; -import { fetchQuerySignals } from './api'; -import { SignalSearchResponse } from './types'; +import { fetchQueryAlerts } from './api'; +import { AlertSearchResponse } from './types'; type Func = () => void; -export interface ReturnQuerySignals { +export interface ReturnQueryAlerts { loading: boolean; - data: SignalSearchResponse | null; + data: AlertSearchResponse | null; setQuery: React.Dispatch>; response: string; request: string; @@ -21,18 +21,18 @@ export interface ReturnQuerySignals { } /** - * Hook for using to get a Signals from the Detection Engine API + * Hook for fetching Alerts from the Detection Engine API * * @param initialQuery query dsl object * */ -export const useQuerySignals = ( +export const useQueryAlerts = ( initialQuery: object, indexName?: string | null -): ReturnQuerySignals => { +): ReturnQueryAlerts => { const [query, setQuery] = useState(initialQuery); - const [signals, setSignals] = useState< - Pick, 'data' | 'setQuery' | 'response' | 'request' | 'refetch'> + const [alerts, setAlerts] = useState< + Pick, 'data' | 'setQuery' | 'response' | 'request' | 'refetch'> >({ data: null, response: '', @@ -49,15 +49,15 @@ export const useQuerySignals = ( async function fetchData() { try { setLoading(true); - const signalResponse = await fetchQuerySignals({ + const alertResponse = await fetchQueryAlerts({ query, signal: abortCtrl.signal, }); if (isSubscribed) { - setSignals({ - data: signalResponse, - response: JSON.stringify(signalResponse, null, 2), + setAlerts({ + data: alertResponse, + response: JSON.stringify(alertResponse, null, 2), request: JSON.stringify({ index: [indexName] ?? [''], body: query }, null, 2), setQuery, refetch: fetchData, @@ -65,7 +65,7 @@ export const useQuerySignals = ( } } catch (error) { if (isSubscribed) { - setSignals({ + setAlerts({ data: null, response: '', request: '', @@ -86,5 +86,5 @@ export const useQuerySignals = ( }; }, [query, indexName]); - return { loading, ...signals }; + return { loading, ...alerts }; }; diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx index c834e4ab14be2..d0571bfca5b2b 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.test.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -30,7 +30,7 @@ describe('useSignalIndex', () => { }); }); - test('fetch signals info', async () => { + test('fetch alerts info', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useSignalIndex() @@ -105,7 +105,7 @@ describe('useSignalIndex', () => { }); }); - test('if there is an error when fetching signals info, signalIndexExists === false && signalIndexName == null', async () => { + test('if there is an error when fetching alerts info, signalIndexExists === false && signalIndexName == null', async () => { const spyOnGetSignalIndex = jest.spyOn(api, 'getSignalIndex'); spyOnGetSignalIndex.mockImplementation(() => { throw new Error('Something went wrong, let see what happen'); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx similarity index 100% rename from x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx index a83a85678bd03..e3eb4666522ad 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx @@ -6,7 +6,6 @@ import { EuiButton, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -15,60 +14,34 @@ import { indicesExistOrDataTemporarilyUnavailable, WithSource, } from '../../../common/containers/source'; -import { AlertsTable } from '../../../common/components/alerts_viewer/alerts_table'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; -import { - getDetectionEngineTabUrl, - getRulesUrl, -} from '../../../common/components/link_to/redirect_to_detection_engine'; +import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; -import { SiemNavigation } from '../../../common/components/navigation'; -import { NavTab } from '../../../common/components/navigation/types'; import { State } from '../../../common/store'; import { inputsSelectors } from '../../../common/store/inputs'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { InputsRange } from '../../../common/store/inputs/model'; -import { AlertsByCategory } from '../../../overview/components/alerts_by_category'; -import { useSignalInfo } from '../../components/signals_info'; -import { SignalsTable } from '../../components/signals'; +import { useAlertInfo } from '../../components/alerts_info'; +import { AlertsTable } from '../../components/alerts_table'; import { NoApiIntegrationKeyCallOut } from '../../components/no_api_integration_callout'; -import { NoWriteSignalsCallOut } from '../../components/no_write_signals_callout'; -import { SignalsHistogramPanel } from '../../components/signals_histogram_panel'; -import { signalsHistogramOptions } from '../../components/signals_histogram_panel/config'; +import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; +import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; +import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; -import { DetectionEngineTab } from './types'; - -const detectionsTabs: Record = { - [DetectionEngineTab.signals]: { - id: DetectionEngineTab.signals, - name: i18n.SIGNAL, - href: getDetectionEngineTabUrl(DetectionEngineTab.signals), - disabled: false, - urlKey: 'detections', - }, - [DetectionEngineTab.alerts]: { - id: DetectionEngineTab.alerts, - name: i18n.ALERT, - href: getDetectionEngineTabUrl(DetectionEngineTab.alerts), - disabled: false, - urlKey: 'detections', - }, -}; export const DetectionEnginePageComponent: React.FC = ({ filters, query, setAbsoluteRangeDatePicker, }) => { - const { tabName = DetectionEngineTab.signals } = useParams(); const { loading, isSignalIndexExists, @@ -79,7 +52,7 @@ export const DetectionEnginePageComponent: React.FC = ({ hasIndexWrite, } = useUserInfo(); - const [lastSignals] = useSignalInfo({}); + const [lastAlerts] = useAlertInfo({}); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -116,7 +89,7 @@ export const DetectionEnginePageComponent: React.FC = ({ return ( <> {hasEncryptionKey != null && !hasEncryptionKey && } - {hasIndexWrite != null && !hasIndexWrite && } + {hasIndexWrite != null && !hasIndexWrite && } {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -127,11 +100,11 @@ export const DetectionEnginePageComponent: React.FC = ({ - {i18n.LAST_SIGNAL} + {i18n.LAST_ALERT} {': '} - {lastSignals} + {lastAlerts} ) } @@ -141,7 +114,7 @@ export const DetectionEnginePageComponent: React.FC = ({ fill href={getRulesUrl()} iconType="gear" - data-test-subj="manage-signal-detection-rules" + data-test-subj="manage-alert-detection-rules" > {i18n.BUTTON_MANAGE_RULES} @@ -150,48 +123,29 @@ export const DetectionEnginePageComponent: React.FC = ({ {({ to, from, deleteQuery, setQuery }) => ( <> - - - {tabName === DetectionEngineTab.signals && ( - <> - - - - - )} - {tabName === DetectionEngineTab.alerts && ( - <> - - - - )} + <> + + + + )} diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx index 756e222c02950..1f9b1373d404d 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx @@ -13,7 +13,6 @@ import { DetectionEnginePage } from './detection_engine'; import { EditRulePage } from './rules/edit'; import { RuleDetailsPage } from './rules/details'; import { RulesPage } from './rules'; -import { DetectionEngineTab } from './types'; const detectionEnginePath = `/:pageName(detections)`; @@ -22,11 +21,7 @@ type Props = Partial> & { url: string }; const DetectionEngineContainerComponent: React.FC = () => ( - + @@ -44,7 +39,7 @@ const DetectionEngineContainerComponent: React.FC = () => ( ( - + )} /> diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts index 1894d0ab1a9e7..d9cbcfc8979a1 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts @@ -611,7 +611,7 @@ describe('helpers', () => { const mockAction = { group: 'default', id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', - params: { message: 'ML Rule generated {{state.signals_count}} signals' }, + params: { message: 'ML Rule generated {{state.signals_count}} alerts' }, actionTypeId: '.slack', }; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/translations.ts index f35b6c8d7b00e..615882d4a7e3b 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/translations.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/translations.ts @@ -13,7 +13,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule. export const BACK_TO_RULES = i18n.translate( 'xpack.siem.detectionEngine.createRule.backToRulesDescription', { - defaultMessage: 'Back to signal detection rules', + defaultMessage: 'Back to detection rules', } ); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx index 74110e25cc940..7197ed397717c 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -42,15 +42,15 @@ import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; -import { SignalsHistogramPanel } from '../../../../components/signals_histogram_panel'; -import { SignalsTable } from '../../../../components/signals'; +import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; +import { AlertsTable } from '../../../../components/alerts_table'; import { useUserInfo } from '../../../../components/user_info'; import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; -import { useSignalInfo } from '../../../../components/signals_info'; +import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; -import { buildSignalsRuleIdFilter } from '../../../../components/signals/default_config'; -import { NoWriteSignalsCallOut } from '../../../../components/no_write_signals_callout'; +import { buildAlertsRuleIdFilter } from '../../../../components/alerts_table/default_config'; +import { NoWriteAlertsCallOut } from '../../../../components/no_write_alerts_callout'; import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../../../../components/rules/read_only_callout'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; @@ -59,7 +59,7 @@ import { getStepsData, redirectToDetections, userHasNoPermissions } from '../hel import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../../common/containers/global_time'; -import { signalsHistogramOptions } from '../../../../components/signals_histogram_panel/config'; +import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { State } from '../../../../../common/store'; import { InputsRange } from '../../../../../common/store/inputs/model'; @@ -72,14 +72,14 @@ import { useMlCapabilities } from '../../../../../common/components/ml_popover/h import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; enum RuleDetailTabs { - signals = 'signals', + alerts = 'alerts', failures = 'failures', } const ruleDetailTabs = [ { - id: RuleDetailTabs.signals, - name: detectionI18n.SIGNAL, + id: RuleDetailTabs.alerts, + name: detectionI18n.ALERT, disabled: false, }, { @@ -107,7 +107,7 @@ export const RuleDetailsPageComponent: FC = ({ const [isLoading, rule] = useRule(ruleId); // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); - const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); + const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null ? getStepsData({ rule, detailsView: true }) @@ -117,7 +117,7 @@ export const RuleDetailsPageComponent: FC = ({ defineRuleData: null, scheduleRuleData: null, }; - const [lastSignals] = useSignalInfo({ ruleId }); + const [lastAlerts] = useAlertInfo({ ruleId }); const mlCapabilities = useMlCapabilities(); // TODO: Refactor license check + hasMlAdminPermissions to common check @@ -166,13 +166,13 @@ export const RuleDetailsPageComponent: FC = ({ [isLoading, rule] ); - const signalDefaultFilters = useMemo( - () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), + const alertDefaultFilters = useMemo( + () => (ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), [ruleId] ); - const signalMergedFilters = useMemo(() => [...signalDefaultFilters, ...filters], [ - signalDefaultFilters, + const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ + alertDefaultFilters, filters, ]); @@ -196,7 +196,7 @@ export const RuleDetailsPageComponent: FC = ({ const ruleError = useMemo( () => rule?.status === 'failed' && - ruleDetailTab === RuleDetailTabs.signals && + ruleDetailTab === RuleDetailTabs.alerts && rule?.last_failure_at != null ? ( = ({ return ( <> - {hasIndexWrite != null && !hasIndexWrite && } + {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } {({ indicesExist, indexPattern }) => { @@ -257,12 +257,12 @@ export const RuleDetailsPageComponent: FC = ({ border subtitle={subTitle} subtitle2={[ - ...(lastSignals != null + ...(lastAlerts != null ? [ <> - {detectionI18n.LAST_SIGNAL} + {detectionI18n.LAST_ALERT} {': '} - {lastSignals} + {lastAlerts} , ] : []), @@ -358,24 +358,24 @@ export const RuleDetailsPageComponent: FC = ({ {tabs} - {ruleDetailTab === RuleDetailTabs.signals && ( + {ruleDetailTab === RuleDetailTabs.alerts && ( <> - {ruleId != null && ( - { diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/translations.ts index fc0a79fa652ff..0fe1106171054 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/translations.ts @@ -6,12 +6,9 @@ import { i18n } from '@kbn/i18n'; -export const BACK_TO_DETECTION_ENGINE = i18n.translate( - 'xpack.siem.detectionEngine.rules.backOptionsHeader', - { - defaultMessage: 'Back to detections', - } -); +export const BACK_TO_ALERTS = i18n.translate('xpack.siem.detectionEngine.rules.backOptionsHeader', { + defaultMessage: 'Back to alerts', +}); export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.importRuleTitle', { defaultMessage: 'Import rule…', @@ -22,7 +19,7 @@ export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.add }); export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { - defaultMessage: 'Signal detection rules', + defaultMessage: 'Detection rules', }); export const ADD_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.addPageTitle', { diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts index 34a521ed32b12..3d2f2dc03946a 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts @@ -19,6 +19,6 @@ describe('getBreadcrumbs', () => { }, [] ) - ).toEqual([{ href: '#/link-to/detections', text: 'Detections' }]); + ).toEqual([{ href: '#/link-to/detections', text: 'Alerts' }]); }); }); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts index 159301a07de78..e5cdbd7123ff4 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts @@ -30,13 +30,6 @@ const getTabBreadcrumb = (pathname: string, search: string[]) => { }; } - if (tabPath === 'signals') { - return { - text: i18nDetections.SIGNAL, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - if (tabPath === 'rules') { return { text: i18nRules.PAGE_TITLE, diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/translations.ts index 008d660be9d88..067399f68d51a 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/translations.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/translations.ts @@ -7,27 +7,23 @@ import { i18n } from '@kbn/i18n'; export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.detectionsPageTitle', { - defaultMessage: 'Detections', + defaultMessage: 'Alerts', }); -export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', { - defaultMessage: 'Last signal', +export const LAST_ALERT = i18n.translate('xpack.siem.detectionEngine.lastAlertTitle', { + defaultMessage: 'Last alert', }); -export const TOTAL_SIGNAL = i18n.translate('xpack.siem.detectionEngine.totalSignalTitle', { +export const TOTAL_ALERT = i18n.translate('xpack.siem.detectionEngine.totalAlertTitle', { defaultMessage: 'Total', }); -export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', { - defaultMessage: 'Detected signals', -}); - export const ALERT = i18n.translate('xpack.siem.detectionEngine.alertTitle', { - defaultMessage: 'External alerts', + defaultMessage: 'Detected alerts', }); export const BUTTON_MANAGE_RULES = i18n.translate('xpack.siem.detectionEngine.buttonManageRules', { - defaultMessage: 'Manage signal detection rules', + defaultMessage: 'Manage detection rules', }); export const PANEL_SUBTITLE_SHOWING = i18n.translate( diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/types.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/types.ts deleted file mode 100644 index d529d99ad3ad4..0000000000000 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum DetectionEngineTab { - signals = 'signals', - alerts = 'alerts', -} diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts index 0037fc3cae628..4fb4e5d30ca7a 100644 --- a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts @@ -258,7 +258,7 @@ export const allowTopN = ({ 'string', ].includes(fieldType); - // TODO: remove this explicit whitelist when the ECS documentation includes signals + // TODO: remove this explicit whitelist when the ECS documentation includes alerts const isWhitelistedNonBrowserField = [ 'signal.ancestors.depth', 'signal.ancestors.id', diff --git a/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx index 8151291679e32..0294d175aef19 100644 --- a/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx @@ -25,7 +25,6 @@ import { RedirectToCreatePage, RedirectToConfigureCasesPage, } from './redirect_to_case'; -import { DetectionEngineTab } from '../../../alerts/pages/detection_engine/types'; import { TimelineType } from '../../../../common/types/timeline'; import { RedirectToManagementPage } from './redirect_to_management'; @@ -89,11 +88,6 @@ export const LinkToPage = React.memo(({ match }) => ( exact path={`${match.url}/:pageName(${SiemPageName.detections})`} /> - ; @@ -20,14 +18,9 @@ export type DetectionEngineComponentProps = RouteComponentProps<{ export const DETECTION_ENGINE_PAGE_NAME = 'detections'; export const RedirectToDetectionEnginePage = ({ - match: { - params: { tabName }, - }, location: { search }, }: DetectionEngineComponentProps) => { - const defaultSelectedTab = DetectionEngineTab.signals; - const selectedTab = tabName ? tabName : defaultSelectedTab; - const to = `/${DETECTION_ENGINE_PAGE_NAME}/${selectedTab}${search}`; + const to = `/${DETECTION_ENGINE_PAGE_NAME}${search}`; return ; }; @@ -66,8 +59,6 @@ const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; export const getDetectionEngineUrl = (search?: string) => `${baseDetectionEngineUrl}${appendSearch(search)}`; -export const getDetectionEngineAlertUrl = (search?: string) => - `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}${appendSearch(search)}`; export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; diff --git a/x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/popover_description.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/popover_description.test.tsx.snap index 46e61f9e939ee..78b05a00cef9b 100644 --- a/x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/popover_description.test.tsx.snap +++ b/x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/popover_description.test.tsx.snap @@ -5,7 +5,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` size="s" > ( diff --git a/x-pack/plugins/siem/public/common/components/top_n/helpers.test.tsx b/x-pack/plugins/siem/public/common/components/top_n/helpers.test.tsx index da0f6f59b533f..a9a0b85202fee 100644 --- a/x-pack/plugins/siem/public/common/components/top_n/helpers.test.tsx +++ b/x-pack/plugins/siem/public/common/components/top_n/helpers.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { allEvents, defaultOptions, getOptions, rawEvents, signalEvents } from './helpers'; +import { allEvents, defaultOptions, getOptions, rawEvents, alertEvents } from './helpers'; describe('getOptions', () => { test(`it returns the default options when 'activeTimelineEventType' is undefined`, () => { @@ -19,7 +19,7 @@ describe('getOptions', () => { expect(getOptions('raw')).toEqual(rawEvents); }); - test(`it returns 'signalEvents' when 'activeTimelineEventType' is 'signal'`, () => { - expect(getOptions('signal')).toEqual(signalEvents); + test(`it returns 'alertEvents' when 'activeTimelineEventType' is 'alert'`, () => { + expect(getOptions('alert')).toEqual(alertEvents); }); }); diff --git a/x-pack/plugins/siem/public/common/components/top_n/helpers.ts b/x-pack/plugins/siem/public/common/components/top_n/helpers.ts index a4226cc58530a..b654eaf17b47b 100644 --- a/x-pack/plugins/siem/public/common/components/top_n/helpers.ts +++ b/x-pack/plugins/siem/public/common/components/top_n/helpers.ts @@ -32,17 +32,17 @@ export const rawEvents: TopNOption[] = [ }, ]; -/** A (stable) array containing only the 'Signal events' option */ -export const signalEvents: TopNOption[] = [ +/** A (stable) array containing only the 'Alert events' option */ +export const alertEvents: TopNOption[] = [ { - value: 'signal', - inputDisplay: i18n.SIGNAL_EVENTS, - 'data-test-subj': 'option-signal', + value: 'alert', + inputDisplay: i18n.ALERT_EVENTS, + 'data-test-subj': 'option-alert', }, ]; /** A (stable) array containing the default Top N options */ -export const defaultOptions = [...rawEvents, ...signalEvents]; +export const defaultOptions = [...rawEvents, ...alertEvents]; /** * Returns the options to be displayed in a Top N view select. When @@ -58,8 +58,8 @@ export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => return allEvents; case 'raw': return rawEvents; - case 'signal': - return signalEvents; + case 'alert': + return alertEvents; default: return defaultOptions; } diff --git a/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx b/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx index d2b38a062091e..0505f55d507b8 100644 --- a/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx @@ -310,13 +310,13 @@ describe('StatefulTopN', () => { }); }); - test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { + test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, () => { const filterManager = new FilterManager(mockUiSettingsForFilterManager); const wrapper = mount( { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.defaultView).toEqual('signal'); + expect(props.defaultView).toEqual('alert'); }); }); diff --git a/x-pack/plugins/siem/public/common/components/top_n/index.tsx b/x-pack/plugins/siem/public/common/components/top_n/index.tsx index a71b27e0bd9cb..f6dc8e7c66ed0 100644 --- a/x-pack/plugins/siem/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/siem/public/common/components/top_n/index.tsx @@ -94,14 +94,14 @@ const StatefulTopNComponent: React.FC = ({ const kibana = useKibana(); // Regarding data from useTimelineTypeContext: - // * `documentType` (e.g. 'signals') may only be populated in some views, - // e.g. the `Signals` view on the `Detections` page. + // * `documentType` (e.g. 'alerts') may only be populated in some views, + // e.g. the `Alerts` view on the `Detections` page. // * `id` (`timelineId`) may only be populated when we are rendered in the // context of the active timeline. - // * `indexToAdd`, which enables the signals index to be appended to + // * `indexToAdd`, which enables the alerts index to be appended to // the `indexPattern` returned by `WithSource`, may only be populated when // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the signals index + // behavior enables the 'All events' view by appending the alerts index // to the index pattern. const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); @@ -135,7 +135,7 @@ const StatefulTopNComponent: React.FC = ({ } data-test-subj="top-n" defaultView={ - documentType?.toLocaleLowerCase() === 'signals' ? 'signal' : options[0].value + documentType?.toLocaleLowerCase() === 'alerts' ? 'alert' : options[0].value } deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} field={field} diff --git a/x-pack/plugins/siem/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/siem/public/common/components/top_n/top_n.test.tsx index 0a35b9e25754c..5e1476f62216b 100644 --- a/x-pack/plugins/siem/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/siem/public/common/components/top_n/top_n.test.tsx @@ -163,11 +163,11 @@ describe('TopN', () => { }); test(`it does NOT render SignalsByCategory when defaultView is 'raw'`, () => { - expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(false); }); }); - describe('signals view', () => { + describe('alerts view', () => { let toggleTopN: () => void; let wrapper: ReactWrapper; @@ -176,7 +176,7 @@ describe('TopN', () => { wrapper = mount( { }); test(`it renders SignalsByCategory when defaultView is 'signal'`, () => { - expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(true); }); test(`it does NOT render EventsByDataset when defaultView is 'signal'`, () => { @@ -242,7 +242,7 @@ describe('TopN', () => { }); test(`it does NOT render SignalsByCategory when defaultView is 'all'`, () => { - expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(false); }); }); }); diff --git a/x-pack/plugins/siem/public/common/components/top_n/translations.ts b/x-pack/plugins/siem/public/common/components/top_n/translations.ts index 7db55fa94d42e..a6873ab0ece7b 100644 --- a/x-pack/plugins/siem/public/common/components/top_n/translations.ts +++ b/x-pack/plugins/siem/public/common/components/top_n/translations.ts @@ -18,6 +18,6 @@ export const RAW_EVENTS = i18n.translate('xpack.siem.topN.rawEventsSelectLabel', defaultMessage: 'Raw events', }); -export const SIGNAL_EVENTS = i18n.translate('xpack.siem.topN.signalEventsSelectLabel', { - defaultMessage: 'Signal events', +export const ALERT_EVENTS = i18n.translate('xpack.siem.topN.alertEventsSelectLabel', { + defaultMessage: 'Alert events', }); diff --git a/x-pack/plugins/siem/public/common/mock/mock_ecs.ts b/x-pack/plugins/siem/public/common/mock/mock_ecs.ts index 7fbbabb29da1b..c897da69caba0 100644 --- a/x-pack/plugins/siem/public/common/mock/mock_ecs.ts +++ b/x-pack/plugins/siem/public/common/mock/mock_ecs.ts @@ -1281,7 +1281,7 @@ export const mockEcsData: Ecs[] = [ }, ]; -export const mockEcsDataWithSignal: Ecs = { +export const mockEcsDataWithAlert: Ecs = { _id: '1', timestamp: '2018-11-05T19:03:25.937Z', host: { diff --git a/x-pack/plugins/siem/public/common/mock/timeline_results.ts b/x-pack/plugins/siem/public/common/mock/timeline_results.ts index 42d14daa11a6b..4eb66acdfad65 100644 --- a/x-pack/plugins/siem/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/common/mock/timeline_results.ts @@ -10,7 +10,7 @@ import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types'; import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query'; -import { CreateTimelineProps } from '../../alerts/components/signals/types'; +import { CreateTimelineProps } from '../../alerts/components/alerts_table/types'; import { TimelineModel } from '../../timelines/store/timeline/model'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; export interface MockedProvidedQuery { @@ -2206,7 +2206,7 @@ export const defaultTimelineProps: CreateTimelineProps = { enabled: true, excluded: false, id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-1', + 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-1', kqlQuery: '', name: '1', queryMatch: { field: '_id', operator: ':', value: '1' }, diff --git a/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx index 9ba150a82ec9a..574260a819071 100644 --- a/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx @@ -11,7 +11,6 @@ import { Position } from '@elastic/charts'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/alerts_viewer/translations'; -import { getDetectionEngineAlertUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -22,7 +21,7 @@ import { Query, } from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../../common/store'; -import { HostsType } from '../../../hosts/store/model'; +import { HostsTableType, HostsType } from '../../../hosts/store/model'; import * as i18n from '../../pages/translations'; import { @@ -32,6 +31,7 @@ import { import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; import { navTabs } from '../../../app/home/home_navigations'; +import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; const ID = 'alertsByCategoryOverview'; @@ -75,11 +75,14 @@ const AlertsByCategoryComponent: React.FC = ({ const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.detections); + const urlSearch = useGetUrlSearch(navTabs.hosts); const alertsCountViewAlertsButton = useMemo( () => ( - + {i18n.VIEW_ALERTS} ), diff --git a/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx index d569fd61afc9c..f2ad45be93522 100644 --- a/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx @@ -6,9 +6,9 @@ import React, { useCallback } from 'react'; -import { SignalsHistogramPanel } from '../../../alerts/components/signals_histogram_panel'; -import { signalsHistogramOptions } from '../../../alerts/components/signals_histogram_panel/config'; -import { useSignalIndex } from '../../../alerts/containers/detection_engine/signals/use_signal_index'; +import { AlertsHistogramPanel } from '../../../alerts/components/alerts_histogram_panel'; +import { alertsHistogramOptions } from '../../../alerts/components/alerts_histogram_panel/config'; +import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; import { SetAbsoluteRangeDatePicker } from '../../../network/pages/types'; import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../../common/store'; @@ -65,10 +65,10 @@ const SignalsByCategoryComponent: React.FC = ({ ); const defaultStackByOption = - signalsHistogramOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; + alertsHistogramOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; return ( - = ({ query={query} signalIndexName={signalIndexName} setQuery={setQuery} - showTotalSignalsCount={true} - showLinkToSignals={onlyField == null ? true : false} - stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} + showTotalAlertsCount={true} + showLinkToAlerts={onlyField == null ? true : false} + stackByOptions={onlyField == null ? alertsHistogramOptions : undefined} legendPosition={'right'} to={to} - title={i18n.SIGNAL_COUNT} + title={i18n.ALERT_COUNT} updateDateRange={updateDateRangeCallback} /> ); diff --git a/x-pack/plugins/siem/public/overview/pages/translations.ts b/x-pack/plugins/siem/public/overview/pages/translations.ts index b7bee15e4c5bf..7c0c00029266c 100644 --- a/x-pack/plugins/siem/public/overview/pages/translations.ts +++ b/x-pack/plugins/siem/public/overview/pages/translations.ts @@ -34,8 +34,8 @@ export const RECENT_TIMELINES = i18n.translate('xpack.siem.overview.recentTimeli defaultMessage: 'Recent timelines', }); -export const SIGNAL_COUNT = i18n.translate('xpack.siem.overview.signalCountTitle', { - defaultMessage: 'Signal count', +export const ALERT_COUNT = i18n.translate('xpack.siem.overview.alertCountTitle', { + defaultMessage: 'Alert count', }); export const TOP = (fieldName: string) => diff --git a/x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx index 1136b7c8d0dc4..d018487a1ccd8 100644 --- a/x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx @@ -16,8 +16,8 @@ import React, { useCallback } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; -import { signalsHeaders } from '../../../alerts/components/signals/default_config'; -import { alertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; +import { alertsHeaders } from '../../../alerts/components/alerts_table/default_config'; +import { alertsHeaders as externalAlertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; import { defaultHeaders as eventsDefaultHeaders } from '../../../common/components/events_viewer/default_headers'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { OnUpdateColumns } from '../timeline/events'; @@ -104,10 +104,10 @@ const TitleRow = React.memo<{ const handleResetColumns = useCallback(() => { let resetDefaultHeaders = defaultHeaders; if (isEventViewer) { - if (timelineTypeContext.documentType?.toLocaleLowerCase() === 'alerts') { + if (timelineTypeContext.documentType?.toLocaleLowerCase() === 'externalAlerts') { + resetDefaultHeaders = externalAlertsHeaders; + } else if (timelineTypeContext.documentType?.toLocaleLowerCase() === 'alerts') { resetDefaultHeaders = alertsHeaders; - } else if (timelineTypeContext.documentType?.toLocaleLowerCase() === 'signals') { - resetDefaultHeaders = signalsHeaders; } else { resetDefaultHeaders = eventsDefaultHeaders; } diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx index 3d1a9075386c1..c52be64f94bf1 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx @@ -9,7 +9,7 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { WithSource } from '../../../common/containers/source'; -import { useSignalIndex } from '../../../alerts/containers/detection_engine/signals/use_signal_index'; +import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx index 85097f93464b3..5a3805af0ca43 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -60,7 +60,7 @@ export const eventTypeOptions: EventTypeOptionItem[] = [ inputDisplay: {i18n.RAW_EVENT}, }, { - value: 'signal', + value: 'alert', inputDisplay: {i18n.SIGNAL_EVENT}, }, ]; diff --git a/x-pack/plugins/siem/public/timelines/containers/index.tsx b/x-pack/plugins/siem/public/timelines/containers/index.tsx index 76f6bdd36ecec..5efcb84539123 100644 --- a/x-pack/plugins/siem/public/timelines/containers/index.tsx +++ b/x-pack/plugins/siem/public/timelines/containers/index.tsx @@ -27,7 +27,7 @@ import { QueryTemplate, QueryTemplateProps } from '../../common/containers/query import { EventType } from '../../timelines/store/timeline/model'; import { timelineQuery } from './index.gql_query'; import { timelineActions } from '../../timelines/store/timeline'; -import { SIGNALS_PAGE_TIMELINE_ID } from '../../alerts/components/signals'; +import { ALERTS_TABLE_TIMELINE_ID } from '../../alerts/components/alerts_table'; export interface TimelineArgs { events: TimelineItem[]; @@ -182,7 +182,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch) => ({ clearSignalsState: ({ id }: { id?: string }) => { - if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { + if (id != null && id === ALERTS_TABLE_TIMELINE_ID) { dispatch(timelineActions.clearEventsLoading({ id })); dispatch(timelineActions.clearEventsDeleted({ id })); } diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/model.ts b/x-pack/plugins/siem/public/timelines/store/timeline/model.ts index bf86d6363861e..f7e848e8a9e1b 100644 --- a/x-pack/plugins/siem/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/model.ts @@ -18,7 +18,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; -export type EventType = 'all' | 'raw' | 'signal'; +export type EventType = 'all' | 'raw' | 'alert'; export type ColumnHeaderType = 'not-filtered' | 'text-filter'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index cce23d3a0d59a..9246d8cb3519b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -641,7 +641,7 @@ export const getEmptySignalsResponse = (): SignalSearchResponse => ({ _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] }, aggregations: { - signalsByGrouping: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + alertsByGrouping: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, }, }); diff --git a/x-pack/plugins/siem/server/lib/matrix_histogram/translations.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/translations.ts index 413acaa2d4b0a..9fdfc9ff7be0d 100644 --- a/x-pack/plugins/siem/server/lib/matrix_histogram/translations.ts +++ b/x-pack/plugins/siem/server/lib/matrix_histogram/translations.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; export const ALL_OTHERS = i18n.translate( - 'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel', + 'xpack.siem.detectionEngine.alerts.histogram.allOthersGroupingLabel', { defaultMessage: 'All others', } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 18186f9a1bfcd..dbb2a85eb2a22 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13853,9 +13853,6 @@ "xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "Elasticから事前にパッケージ化されているルールをインストールすることができませんでした", "xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Elasticから事前にパッケージ化されているルールをインストールしました", "xpack.siem.containers.detectionEngine.rules": "ルールを取得できませんでした", - "xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription": "シグナルをクエリできませんでした", - "xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription": "シグナルインデックス名を取得できませんでした", - "xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription": "シグナルインデックスを作成できませんでした", "xpack.siem.containers.detectionEngine.tagFetchFailDescription": "タグを取得できませんでした", "xpack.siem.containers.errors.dataFetchFailureTitle": "データの取得に失敗", "xpack.siem.containers.errors.networkFailureTitle": "ネットワーク障害", @@ -13972,7 +13969,6 @@ "xpack.siem.detectionEngine.detectionsPageTitle": "検出", "xpack.siem.detectionEngine.dismissButton": "閉じる", "xpack.siem.detectionEngine.dismissNoApiIntegrationKeyButton": "閉じる", - "xpack.siem.detectionEngine.dismissNoWriteSignalButton": "閉じる", "xpack.siem.detectionEngine.editRule.backToDescription": "戻る", "xpack.siem.detectionEngine.editRule.cancelTitle": "キャンセル", "xpack.siem.detectionEngine.editRule.errorMsgDescription": "申し訳ありません", @@ -13984,7 +13980,6 @@ "xpack.siem.detectionEngine.goToDocumentationButton": "ドキュメンテーションを表示", "xpack.siem.detectionEngine.headerPage.pageBadgeLabel": "ベータ", "xpack.siem.detectionEngine.headerPage.pageBadgeTooltip": "検出はまだベータ段階です。Kibana repoで問題やバグを報告して、製品の改善にご協力ください。", - "xpack.siem.detectionEngine.lastSignalTitle": "前回のシグナル", "xpack.siem.detectionEngine.mitreAttack.addTitle": "MITRE ATT&CK\\u2122脅威を追加", "xpack.siem.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "Tacticを追加...", "xpack.siem.detectionEngine.mitreAttack.tacticsDescription": "Tactic", @@ -14274,8 +14269,6 @@ "xpack.siem.detectionEngine.noApiIntegrationKeyCallOutTitle": "API統合キーが必要です", "xpack.siem.detectionEngine.noIndexMsgBody": "検出エンジンを使用するには、必要なクラスターとインデックス権限のユーザーが最初にこのページにアクセスする必要があります。ヘルプについては、管理者にお問い合わせください。", "xpack.siem.detectionEngine.noIndexTitle": "検出エンジンを設定しましょう", - "xpack.siem.detectionEngine.noWriteSignalsCallOutMsg": "現在、シグナルを更新するための必要な権限がありません。サポートについては、管理者にお問い合わせください。", - "xpack.siem.detectionEngine.noWriteSignalsCallOutTitle": "シグナルインデックス権限が必要です", "xpack.siem.detectionEngine.pageTitle": "検出エンジン", "xpack.siem.detectionEngine.panelSubtitleShowing": "表示中", "xpack.siem.detectionEngine.readOnlyCallOutMsg": "現在、検出エンジンルールを作成/編集するための必要な権限がありません。サポートについては、管理者にお問い合わせください。", @@ -14380,43 +14373,6 @@ "xpack.siem.detectionEngine.ruleStatus.statusDateDescription": "ステータス日付", "xpack.siem.detectionEngine.ruleStatus.statusDescription": "前回の応答", "xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default": "デフォルト", - "xpack.siem.detectionEngine.signals.actions.closeSignalTitle": "シグナルを閉じる", - "xpack.siem.detectionEngine.signals.actions.investigateInTimelineTitle": "タイムラインで調査", - "xpack.siem.detectionEngine.signals.actions.openSignalTitle": "シグナルを開く", - "xpack.siem.detectionEngine.signals.closedSignalsTitle": "閉じたシグナル", - "xpack.siem.detectionEngine.signals.documentTypeTitle": "シグナル", - "xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel": "その他すべて", - "xpack.siem.detectionEngine.signals.histogram.headerTitle": "シグナル数", - "xpack.siem.detectionEngine.signals.histogram.showingSignalsTitle": "表示中: {modifier}{totalSignalsFormatted} {totalSignals, plural, =1 {シグナル} other {シグナル}}", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.destinationIpsDropDown": "上位のデスティネーションIP", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventActionsDropDown": "上位のイベントアクション", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventCategoriesDropDown": "上位のイベントカテゴリー", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.hostNamesDropDown": "上位のホスト名", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.riskScoresDropDown": "リスクスコア", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.rulesDropDown": "上位のルール", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.ruleTypesDropDown": "上位のルールタイプ", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.severitiesDropDown": "重要度", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.sourceIpsDropDown": "上位のソースIP", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.stackByLabel": "積み上げ", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.usersDropDown": "上位のユーザー", - "xpack.siem.detectionEngine.signals.histogram.topNLabel": "トップ{fieldName}", - "xpack.siem.detectionEngine.signals.histogram.viewSignalsButtonLabel": "シグナルの表示", - "xpack.siem.detectionEngine.signals.loadingSignalsTitle": "シグナルの読み込み中", - "xpack.siem.detectionEngine.signals.openSignalsTitle": "シグナルを開く", - "xpack.siem.detectionEngine.signals.tableTitle": "シグナル", - "xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle": "シグナルが検索条件に一致します", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.closeSelectedTitle": "選択した項目を閉じる", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.openSelectedTitle": "選択した項目を開く", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInHostsTitle": "ホストで選択した項目を表示", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInNetworkTitle": "ネットワークで選択した項目を表示", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInTimelineTitle": "タイムラインで選択した項目を表示", - "xpack.siem.detectionEngine.signals.utilityBar.batchActionsTitle": "バッチ処理", - "xpack.siem.detectionEngine.signals.utilityBar.clearSelectionTitle": "選択した項目をクリア", - "xpack.siem.detectionEngine.signals.utilityBar.selectAllSignalsTitle": "すべての{totalSignalsFormatted} {totalSignals, plural, =1 {シグナル} other {シグナル}}を選択", - "xpack.siem.detectionEngine.signals.utilityBar.selectedSignalsTitle": "{selectedSignalsFormatted} {selectedSignals, plural, =1 {シグナル} other {シグナル}}を選択しました", - "xpack.siem.detectionEngine.signals.utilityBar.showingSignalsTitle": "{totalSignalsFormatted} {totalSignals, plural, =1 {シグナル} other {シグナル}}を表示中", - "xpack.siem.detectionEngine.signalTitle": "検出したシグナル", - "xpack.siem.detectionEngine.totalSignalTitle": "合計", "xpack.siem.detectionEngine.userUnauthenticatedMsgBody": "検出エンジンを表示するための必要なアクセス権がありません。ヘルプについては、管理者にお問い合わせください。", "xpack.siem.detectionEngine.userUnauthenticatedTitle": "検出エンジンアクセス権が必要です", "xpack.siem.dragAndDrop.addToTimeline": "タイムライン調査に追加", @@ -14456,11 +14412,6 @@ "xpack.siem.eventsViewer.eventsLabel": "イベント", "xpack.siem.eventsViewer.footer.loadingEventsDataLabel": "イベントを読み込み中", "xpack.siem.eventsViewer.showingLabel": "表示中", - "xpack.siem.eventsViewer.signals.defaultHeaders.methodTitle": "メソド", - "xpack.siem.eventsViewer.signals.defaultHeaders.riskScoreTitle": "リスクスコア", - "xpack.siem.eventsViewer.signals.defaultHeaders.ruleTitle": "ルール", - "xpack.siem.eventsViewer.signals.defaultHeaders.severityTitle": "深刻度", - "xpack.siem.eventsViewer.signals.defaultHeaders.versionTitle": "バージョン", "xpack.siem.eventsViewer.unit": "{totalCount, plural, =1 {event} other {events}}", "xpack.siem.featureCatalogue.description": "セキュリティメトリクスとログのイベントとアラートを確認します", "xpack.siem.featureCatalogue.title": "SIEM", @@ -14793,7 +14744,6 @@ "xpack.siem.overview.recentlyCreatedCasesButtonLabel": "最近作成したケース", "xpack.siem.overview.recentTimelinesSidebarTitle": "最近のタイムライン", "xpack.siem.overview.showTopTooltip": "上位の{fieldName}を表示", - "xpack.siem.overview.signalCountTitle": "シグナル数", "xpack.siem.overview.startedText": "セキュリティ情報およびイベント管理(SIEM)へようこそ。はじめに{docs}や{data}をご参照ください。今後の機能に関する情報やチュートリアルは、{siemSolution} ページをお見逃しなく。", "xpack.siem.overview.startedText.dataLinkText": "投入データ", "xpack.siem.overview.startedText.docsLinkText": "ドキュメンテーション", @@ -14974,7 +14924,6 @@ "xpack.siem.topN.allEventsSelectLabel": "すべてのイベント", "xpack.siem.topN.closeButtonLabel": "閉じる", "xpack.siem.topN.rawEventsSelectLabel": "未加工イベント", - "xpack.siem.topN.signalEventsSelectLabel": "シグナルイベント", "xpack.siem.uiSettings.defaultAnomalyScoreDescription": "

    機械学習ジョブの異常がこの値を超えると SIEM アプリに表示されます。

    有効な値:0 ~ 100。

    ", "xpack.siem.uiSettings.defaultAnomalyScoreLabel": "デフォルトの異常しきい値", "xpack.siem.uiSettings.defaultIndexDescription": "

    SIEM アプリがイベントを収集する Elasticsearch インデックスのコンマ区切りのリストです。

    ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 195309f640a0a..5527385c752e3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13858,9 +13858,6 @@ "xpack.siem.containers.detectionEngine.createPrePackagedRuleFailDescription": "无法安装 elastic 的预打包规则", "xpack.siem.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "已安装 elastic 的预打包规则", "xpack.siem.containers.detectionEngine.rules": "无法提取规则", - "xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription": "无法查询信号", - "xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription": "无法获取信号索引名称", - "xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription": "无法创建信号索引", "xpack.siem.containers.detectionEngine.tagFetchFailDescription": "无法提取标记", "xpack.siem.containers.errors.dataFetchFailureTitle": "数据提取失败", "xpack.siem.containers.errors.networkFailureTitle": "网络故障", @@ -13977,7 +13974,6 @@ "xpack.siem.detectionEngine.detectionsPageTitle": "检测", "xpack.siem.detectionEngine.dismissButton": "关闭", "xpack.siem.detectionEngine.dismissNoApiIntegrationKeyButton": "关闭", - "xpack.siem.detectionEngine.dismissNoWriteSignalButton": "关闭", "xpack.siem.detectionEngine.editRule.backToDescription": "返回到", "xpack.siem.detectionEngine.editRule.cancelTitle": "取消", "xpack.siem.detectionEngine.editRule.errorMsgDescription": "抱歉", @@ -13989,7 +13985,6 @@ "xpack.siem.detectionEngine.goToDocumentationButton": "查看文档", "xpack.siem.detectionEngine.headerPage.pageBadgeLabel": "公测版", "xpack.siem.detectionEngine.headerPage.pageBadgeTooltip": "“检测”仍为公测版。请通过在 Kibana 存储库中报告问题或错误,帮助我们改进产品。", - "xpack.siem.detectionEngine.lastSignalTitle": "上一信号", "xpack.siem.detectionEngine.mitreAttack.addTitle": "添加 MITRE ATT&CK\\u2122 威胁", "xpack.siem.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "选择策略......", "xpack.siem.detectionEngine.mitreAttack.tacticsDescription": "策略", @@ -14279,8 +14274,6 @@ "xpack.siem.detectionEngine.noApiIntegrationKeyCallOutTitle": "需要 API 集成密钥", "xpack.siem.detectionEngine.noIndexMsgBody": "要使用检测引擎,具有所需集群和索引权限的用户必须首先访问此页面。若需要更多帮助,请联系您的管理员。", "xpack.siem.detectionEngine.noIndexTitle": "让我们来设置您的检测引擎", - "xpack.siem.detectionEngine.noWriteSignalsCallOutMsg": "您当前缺少所需的权限,无法更新信号。有关进一步帮助,请联系您的管理员。", - "xpack.siem.detectionEngine.noWriteSignalsCallOutTitle": "需要信号索引权限", "xpack.siem.detectionEngine.pageTitle": "检测引擎", "xpack.siem.detectionEngine.panelSubtitleShowing": "正在显示", "xpack.siem.detectionEngine.readOnlyCallOutMsg": "您当前缺少所需的权限,无法创建/编辑检测引擎规则。有关进一步帮助,请联系您的管理员。", @@ -14385,43 +14378,6 @@ "xpack.siem.detectionEngine.ruleStatus.statusDateDescription": "状态日期", "xpack.siem.detectionEngine.ruleStatus.statusDescription": "上次响应", "xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default": "默认值", - "xpack.siem.detectionEngine.signals.actions.closeSignalTitle": "关闭信号", - "xpack.siem.detectionEngine.signals.actions.investigateInTimelineTitle": "在时间线中调查", - "xpack.siem.detectionEngine.signals.actions.openSignalTitle": "打开信号", - "xpack.siem.detectionEngine.signals.closedSignalsTitle": "已关闭信号", - "xpack.siem.detectionEngine.signals.documentTypeTitle": "信号", - "xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel": "所有其他", - "xpack.siem.detectionEngine.signals.histogram.headerTitle": "信号计数", - "xpack.siem.detectionEngine.signals.histogram.showingSignalsTitle": "正在显示:{modifier}{totalSignalsFormatted} 个{totalSignals, plural, =1 {信号} other {信号}}", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.destinationIpsDropDown": "排名靠前的目标 IP", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventActionsDropDown": "排名靠前的事件操作", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventCategoriesDropDown": "排名靠前的事件类别", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.hostNamesDropDown": "排名靠前的主机名", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.riskScoresDropDown": "风险分数", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.rulesDropDown": "排名靠前的规则", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.ruleTypesDropDown": "排名靠前的规则类型", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.severitiesDropDown": "严重性", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.sourceIpsDropDown": "排名靠前的源 IP", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.stackByLabel": "堆叠依据", - "xpack.siem.detectionEngine.signals.histogram.stackByOptions.usersDropDown": "排名靠前的用户", - "xpack.siem.detectionEngine.signals.histogram.topNLabel": "热门{fieldName}", - "xpack.siem.detectionEngine.signals.histogram.viewSignalsButtonLabel": "查看信号", - "xpack.siem.detectionEngine.signals.loadingSignalsTitle": "正在加载信号", - "xpack.siem.detectionEngine.signals.openSignalsTitle": "打开信号", - "xpack.siem.detectionEngine.signals.tableTitle": "信号", - "xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle": "个信号匹配搜索条件", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.closeSelectedTitle": "关闭选定", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.openSelectedTitle": "打开选定", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInHostsTitle": "查看主机中所选", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInNetworkTitle": "查看网络中所选", - "xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInTimelineTitle": "查看时间线中所选", - "xpack.siem.detectionEngine.signals.utilityBar.batchActionsTitle": "批量操作", - "xpack.siem.detectionEngine.signals.utilityBar.clearSelectionTitle": "清除选择", - "xpack.siem.detectionEngine.signals.utilityBar.selectAllSignalsTitle": "选择所有 {totalSignalsFormatted} 个{totalSignals, plural, =1 {信号} other {信号}}", - "xpack.siem.detectionEngine.signals.utilityBar.selectedSignalsTitle": "已选择 {selectedSignalsFormatted} 个{selectedSignals, plural, =1 {信号} other {信号}}", - "xpack.siem.detectionEngine.signals.utilityBar.showingSignalsTitle": "正在显示 {totalSignalsFormatted} 个{totalSignals, plural, =1 {信号} other {信号}}", - "xpack.siem.detectionEngine.signalTitle": "检测到的信号", - "xpack.siem.detectionEngine.totalSignalTitle": "合计", "xpack.siem.detectionEngine.userUnauthenticatedMsgBody": "您没有所需的权限,无法查看检测引擎。若需要更多帮助,请联系您的管理员。", "xpack.siem.detectionEngine.userUnauthenticatedTitle": "需要检测引擎权限", "xpack.siem.dragAndDrop.addToTimeline": "添加到时间线调查", @@ -14461,11 +14417,6 @@ "xpack.siem.eventsViewer.eventsLabel": "事件", "xpack.siem.eventsViewer.footer.loadingEventsDataLabel": "正在加载事件", "xpack.siem.eventsViewer.showingLabel": "显示", - "xpack.siem.eventsViewer.signals.defaultHeaders.methodTitle": "方法", - "xpack.siem.eventsViewer.signals.defaultHeaders.riskScoreTitle": "风险分数", - "xpack.siem.eventsViewer.signals.defaultHeaders.ruleTitle": "规则", - "xpack.siem.eventsViewer.signals.defaultHeaders.severityTitle": "严重性", - "xpack.siem.eventsViewer.signals.defaultHeaders.versionTitle": "版本", "xpack.siem.eventsViewer.unit": "{totalCount, plural, =1 {个事件} other {个事件}}", "xpack.siem.featureCatalogue.description": "浏览安全指标和日志以了解事件和告警", "xpack.siem.featureCatalogue.title": "SIEM", @@ -14798,7 +14749,6 @@ "xpack.siem.overview.recentlyCreatedCasesButtonLabel": "最近创建的案例", "xpack.siem.overview.recentTimelinesSidebarTitle": "最近的时间线", "xpack.siem.overview.showTopTooltip": "显示热门{fieldName}", - "xpack.siem.overview.signalCountTitle": "信号计数", "xpack.siem.overview.startedText": "欢迎使用安全信息和事件管理 (SIEM)。首先,查看我们的 {docs} 或 {data}。有关即将推出的功能和教程,确保查看我们的{siemSolution}页。", "xpack.siem.overview.startedText.dataLinkText": "正在采集数据", "xpack.siem.overview.startedText.docsLinkText": "文档", @@ -14979,7 +14929,6 @@ "xpack.siem.topN.allEventsSelectLabel": "所有事件", "xpack.siem.topN.closeButtonLabel": "关闭", "xpack.siem.topN.rawEventsSelectLabel": "原始事件", - "xpack.siem.topN.signalEventsSelectLabel": "信号事件", "xpack.siem.uiSettings.defaultAnomalyScoreDescription": "

    在显示异常之前要超过的默认异常分数阈值。

    有效值:0 到 100。

    ", "xpack.siem.uiSettings.defaultAnomalyScoreLabel": "默认异常阈值", "xpack.siem.uiSettings.defaultIndexDescription": "

    SIEM 应用要从其中搜索事件的 Elasticsearch 索引逗号分隔列表。

    ", diff --git a/x-pack/test/siem_cypress/es_archives/signals/data.json.gz b/x-pack/test/siem_cypress/es_archives/alerts/data.json.gz similarity index 100% rename from x-pack/test/siem_cypress/es_archives/signals/data.json.gz rename to x-pack/test/siem_cypress/es_archives/alerts/data.json.gz diff --git a/x-pack/test/siem_cypress/es_archives/signals/mappings.json b/x-pack/test/siem_cypress/es_archives/alerts/mappings.json similarity index 100% rename from x-pack/test/siem_cypress/es_archives/signals/mappings.json rename to x-pack/test/siem_cypress/es_archives/alerts/mappings.json diff --git a/x-pack/test/siem_cypress/es_archives/closed_signals/data.json.gz b/x-pack/test/siem_cypress/es_archives/closed_alerts/data.json.gz similarity index 100% rename from x-pack/test/siem_cypress/es_archives/closed_signals/data.json.gz rename to x-pack/test/siem_cypress/es_archives/closed_alerts/data.json.gz diff --git a/x-pack/test/siem_cypress/es_archives/closed_signals/mappings.json b/x-pack/test/siem_cypress/es_archives/closed_alerts/mappings.json similarity index 100% rename from x-pack/test/siem_cypress/es_archives/closed_signals/mappings.json rename to x-pack/test/siem_cypress/es_archives/closed_alerts/mappings.json diff --git a/x-pack/test/siem_cypress/es_archives/timeline_signals/data.json.gz b/x-pack/test/siem_cypress/es_archives/timeline_alerts/data.json.gz similarity index 100% rename from x-pack/test/siem_cypress/es_archives/timeline_signals/data.json.gz rename to x-pack/test/siem_cypress/es_archives/timeline_alerts/data.json.gz diff --git a/x-pack/test/siem_cypress/es_archives/timeline_signals/mappings.json b/x-pack/test/siem_cypress/es_archives/timeline_alerts/mappings.json similarity index 100% rename from x-pack/test/siem_cypress/es_archives/timeline_signals/mappings.json rename to x-pack/test/siem_cypress/es_archives/timeline_alerts/mappings.json From 36c6cd941549baecacfa8b86fd30c7d7a8edd61d Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 3 Jun 2020 06:41:18 +0200 Subject: [PATCH 65/69] [Discover] Fix missing histogram time range update when query is refreshed (#67582) --- .../public/application/angular/discover.js | 1 + test/functional/apps/discover/_discover.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index daea8b5938042..f9ccdbf5ab535 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -870,6 +870,7 @@ function discoverController( if ($scope.vis.data.aggs.aggs[1]) { $scope.bucketInterval = $scope.vis.data.aggs.aggs[1].buckets.getInterval(); } + $scope.updateTime(); } $scope.hits = resp.hits.total; diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 163022e32c82f..9f3ce667d64b5 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -233,5 +233,19 @@ export default function ({ getService, getPageObjects }) { expect(await PageObjects.discover.getNrOfFetches()).to.be(1); }); }); + + describe('empty query', function () { + it('should update the histogram timerange when the query is resubmitted', async function () { + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', + }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.awaitKibanaChrome(); + const initialTimeString = await PageObjects.discover.getChartTimespan(); + await queryBar.submitQuery(); + const refreshedTimeString = await PageObjects.discover.getChartTimespan(); + expect(refreshedTimeString).not.to.be(initialTimeString); + }); + }); }); } From fbcb74ce285a1d35af153fe8422fcfa6bd860557 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 3 Jun 2020 09:35:44 +0300 Subject: [PATCH 66/69] Add error if filter index pattern is missing (#66979) * add error if filter index pattern is gone * docs change - why? * Fix i18n * Added a functional test for broken filters (field + index pattern) * Clarify readme * Moved readme * New warning status * Remove getAll * git pull upstream master * Fix translation files * Fix merge * added filterbar texts * disabled correction * Disable check in maps test until #64861 is resolved * Fix tests, warning state is not disabled. * Adjust warning filter - ignore filters from "foreign" index pattern, that are still applicable * Add an additional unrelaeted filter test * Update src/plugins/data/public/ui/filter_bar/_global_filter_item.scss Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> * Update src/plugins/data/public/ui/filter_bar/_global_filter_item.scss Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> * Fixed test data * Revert mapping * Update data with missing test * Update test to match data * Code review Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../es_query/filters/get_display_value.ts | 4 +- .../filter_manager/compare_filters.test.ts | 16 + .../query/filter_manager/compare_filters.ts | 4 + .../ui/filter_bar/_global_filter_item.scss | 15 +- .../data/public/ui/filter_bar/filter_bar.tsx | 1 + .../ui/filter_bar/filter_editor/index.tsx | 7 +- .../data/public/ui/filter_bar/filter_item.tsx | 322 +++++++++++------- .../ui/filter_bar/filter_view/index.tsx | 12 +- .../apps/dashboard/dashboard_filter_bar.js | 32 ++ .../functional/fixtures/es_archiver/README.md | 10 + .../dashboard/current/kibana/data.json.gz | Bin 16643 -> 20510 bytes .../dashboard/current/kibana/mappings.json | 230 ++++++++++++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../maps/embeddable/tooltip_filter_actions.js | 5 +- 15 files changed, 525 insertions(+), 135 deletions(-) create mode 100644 test/functional/fixtures/es_archiver/README.md diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 03167f3080419..10b4dab3f46ef 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -25,6 +25,7 @@ import { Filter } from '../filters'; function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { if (!indexPattern || !key) return; + let format = get(indexPattern, ['fields', 'byName', key, 'format']); if (!format && (indexPattern.fields as any).getByName) { // TODO: Why is indexPatterns sometimes a map and sometimes an array? @@ -43,9 +44,8 @@ function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { } export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IIndexPattern[]): string { - const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); - if (typeof filter.meta.value === 'function') { + const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); const valueFormatter: any = getValueFormatter(indexPattern, filter.meta.key); return filter.meta.value(valueFormatter); } else { diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts index 0c3947ade8221..1e5391332e6b0 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -228,5 +228,21 @@ describe('filter manager utilities', () => { expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeFalsy(); }); + + test('should compare index with index true', () => { + const f1 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + const f2 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + + f2.meta.index = 'wassup'; + f2.meta.index = 'dog'; + + expect(compareFilters([f1], [f2], { index: true })).toBeFalsy(); + }); }); }); diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts index 3be52a9a60977..65df6e26a25b3 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -21,6 +21,7 @@ import { defaults, isEqual, omit, map } from 'lodash'; import { FilterMeta, Filter } from '../../es_query'; export interface FilterCompareOptions { + index?: boolean; disabled?: boolean; negate?: boolean; state?: boolean; @@ -31,6 +32,7 @@ export interface FilterCompareOptions { * Include disabled, negate and store when comparing filters */ export const COMPARE_ALL_OPTIONS: FilterCompareOptions = { + index: true, disabled: true, negate: true, state: true, @@ -44,6 +46,7 @@ const mapFilter = ( ) => { const cleaned: FilterMeta = omit(filter, excludedAttributes); + if (comparators.index) cleaned.index = filter.meta?.index; if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); if (comparators.alias) cleaned.alias = filter.meta?.alias; @@ -81,6 +84,7 @@ export const compareFilters = ( const excludedAttributes: string[] = ['$$hashKey', 'meta']; comparators = defaults(comparatorOptions || {}, { + index: false, state: false, negate: false, disabled: false, diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 24adf0093af95..73ec14de82b43 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -32,15 +32,26 @@ font-style: italic; } -.globalFilterItem-isInvalid { +.globalFilterItem-isError, .globalFilterItem-isWarning { text-decoration: none; .globalFilterLabel__value { - color: $euiColorDanger; font-weight: $euiFontWeightBold; } } +.globalFilterItem-isError { + .globalFilterLabel__value { + color: makeHighContrastColor($euiColorDangerText, $euiColorLightShade); + } +} + +.globalFilterItem-isWarning { + .globalFilterLabel__value { + color: makeHighContrastColor($euiColorWarningText, $euiColorLightShade); + } +} + .globalFilterItem-isPinned { position: relative; diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index d89cf01eedd43..a54a25acc5913 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -64,6 +64,7 @@ function FilterBarUI(props: Props) { onUpdate(i, newFilter)} onRemove={() => onRemove(i)} diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index fd228a2213795..0e2bcc7581950 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -198,9 +198,14 @@ class FilterEditorUI extends Component { if ( this.props.indexPatterns.length <= 1 && this.props.indexPatterns.find( - (indexPattern) => indexPattern === this.state.selectedIndexPattern + (indexPattern) => indexPattern === this.getIndexPatternFromFilter() ) ) { + /** + * Don't render the index pattern selector if there's just one \ zero index patterns + * and if the index pattern the filter was LOADED with is in the indexPatterns list. + **/ + return ''; } const { selectedIndexPattern } = this.state; diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 528ec4800e7b9..c44e1faeb8e7f 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -18,9 +18,9 @@ */ import { EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { InjectedIntl } from '@kbn/i18n/react'; import classNames from 'classnames'; -import React, { Component, MouseEvent } from 'react'; +import React, { MouseEvent, useState, useEffect } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; @@ -32,8 +32,9 @@ import { toggleFilterNegated, toggleFilterPinned, toggleFilterDisabled, + getIndexPatternFromFilter, } from '../../../common'; -import { getNotifications } from '../../services'; +import { getIndexPatterns } from '../../services'; interface Props { id: string; @@ -46,95 +47,123 @@ interface Props { uiSettings: IUiSettingsClient; } -interface State { - isPopoverOpen: boolean; +interface LabelOptions { + title: string; + status: string; + message?: string; } -class FilterItemUI extends Component { - public state = { - isPopoverOpen: false, - }; +const FILTER_ITEM_OK = ''; +const FILTER_ITEM_WARNING = 'warn'; +const FILTER_ITEM_ERROR = 'error'; - private handleBadgeClick = (e: MouseEvent) => { - if (e.shiftKey) { - this.onToggleDisabled(); +export function FilterItem(props: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [indexPatternExists, setIndexPatternExists] = useState(undefined); + const { id, filter, indexPatterns } = props; + + useEffect(() => { + const index = props.filter.meta.index; + if (index) { + getIndexPatterns() + .get(index) + .then((indexPattern) => { + setIndexPatternExists(!!indexPattern); + }) + .catch(() => { + setIndexPatternExists(false); + }); } else { - this.togglePopover(); + setIndexPatternExists(false); } - }; - public render() { - const { filter, id } = this.props; - const { negate, disabled } = filter.meta; - let hasError: boolean = false; + }, [props.filter.meta.index]); - let valueLabel; - try { - valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); - } catch (e) { - getNotifications().toasts.addError(e, { - title: this.props.intl.formatMessage({ - id: 'data.filter.filterBar.labelErrorMessage', - defaultMessage: 'Failed to display filter', - }), - }); - valueLabel = this.props.intl.formatMessage({ - id: 'data.filter.filterBar.labelErrorText', - defaultMessage: 'Error', - }); - hasError = true; + function handleBadgeClick(e: MouseEvent) { + if (e.shiftKey) { + onToggleDisabled(); + } else { + setIsPopoverOpen(!isPopoverOpen); } - const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; - const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; - const dataTestSubjDisabled = `filter-${ - this.props.filter.meta.disabled ? 'disabled' : 'enabled' - }`; - const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; + } - const classes = classNames( + function onSubmit(f: Filter) { + setIsPopoverOpen(false); + props.onUpdate(f); + } + + function onTogglePinned() { + const f = toggleFilterPinned(filter); + props.onUpdate(f); + } + + function onToggleNegated() { + const f = toggleFilterNegated(filter); + props.onUpdate(f); + } + + function onToggleDisabled() { + const f = toggleFilterDisabled(filter); + props.onUpdate(f); + } + + function isValidLabel(labelConfig: LabelOptions) { + return labelConfig.status === FILTER_ITEM_OK; + } + + function isDisabled(labelConfig: LabelOptions) { + const { disabled } = filter.meta; + return disabled || labelConfig.status === FILTER_ITEM_ERROR; + } + + function getClasses(negate: boolean, labelConfig: LabelOptions) { + return classNames( 'globalFilterItem', { - 'globalFilterItem-isDisabled': disabled || hasError, - 'globalFilterItem-isInvalid': hasError, + 'globalFilterItem-isDisabled': isDisabled(labelConfig), + 'globalFilterItem-isError': labelConfig.status === FILTER_ITEM_ERROR, + 'globalFilterItem-isWarning': labelConfig.status === FILTER_ITEM_WARNING, 'globalFilterItem-isPinned': isFilterPinned(filter), 'globalFilterItem-isExcluded': negate, }, - this.props.className + props.className ); + } - const badge = ( - this.props.onRemove()} - onClick={this.handleBadgeClick} - data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjPinned}`} - /> - ); + function getDataTestSubj(labelConfig: LabelOptions) { + const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; + const dataTestSubjValue = filter.meta.value + ? `filter-value-${isValidLabel(labelConfig) ? labelConfig.title : labelConfig.status}` + : ''; + const dataTestSubjDisabled = `filter-${isDisabled(labelConfig) ? 'disabled' : 'enabled'}`; + const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; + return `filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjPinned}`; + } - const panelTree = [ + function getPanels() { + const { negate, disabled } = filter.meta; + return [ { id: 0, items: [ { name: isFilterPinned(filter) - ? this.props.intl.formatMessage({ + ? props.intl.formatMessage({ id: 'data.filter.filterBar.unpinFilterButtonLabel', defaultMessage: 'Unpin', }) - : this.props.intl.formatMessage({ + : props.intl.formatMessage({ id: 'data.filter.filterBar.pinFilterButtonLabel', defaultMessage: 'Pin across all apps', }), icon: 'pin', onClick: () => { - this.closePopover(); - this.onTogglePinned(); + setIsPopoverOpen(false); + onTogglePinned(); }, 'data-test-subj': 'pinFilter', }, { - name: this.props.intl.formatMessage({ + name: props.intl.formatMessage({ id: 'data.filter.filterBar.editFilterButtonLabel', defaultMessage: 'Edit filter', }), @@ -144,47 +173,47 @@ class FilterItemUI extends Component { }, { name: negate - ? this.props.intl.formatMessage({ + ? props.intl.formatMessage({ id: 'data.filter.filterBar.includeFilterButtonLabel', defaultMessage: 'Include results', }) - : this.props.intl.formatMessage({ + : props.intl.formatMessage({ id: 'data.filter.filterBar.excludeFilterButtonLabel', defaultMessage: 'Exclude results', }), icon: negate ? 'plusInCircle' : 'minusInCircle', onClick: () => { - this.closePopover(); - this.onToggleNegated(); + setIsPopoverOpen(false); + onToggleNegated(); }, 'data-test-subj': 'negateFilter', }, { name: disabled - ? this.props.intl.formatMessage({ + ? props.intl.formatMessage({ id: 'data.filter.filterBar.enableFilterButtonLabel', defaultMessage: 'Re-enable', }) - : this.props.intl.formatMessage({ + : props.intl.formatMessage({ id: 'data.filter.filterBar.disableFilterButtonLabel', defaultMessage: 'Temporarily disable', }), icon: `${disabled ? 'eye' : 'eyeClosed'}`, onClick: () => { - this.closePopover(); - this.onToggleDisabled(); + setIsPopoverOpen(false); + onToggleDisabled(); }, 'data-test-subj': 'disableFilter', }, { - name: this.props.intl.formatMessage({ + name: props.intl.formatMessage({ id: 'data.filter.filterBar.deleteFilterButtonLabel', defaultMessage: 'Delete', }), icon: 'trash', onClick: () => { - this.closePopover(); - this.props.onRemove(); + setIsPopoverOpen(false); + props.onRemove(); }, 'data-test-subj': 'deleteFilter', }, @@ -197,63 +226,124 @@ class FilterItemUI extends Component {
    { + setIsPopoverOpen(false); + }} />
    ), }, ]; - - return ( - - - - ); } - private closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; + /** + * Checks if filter field exists in any of the index patterns provided, + * Because if so, a filter for the wrong index pattern may still be applied. + * This function makes this behavior explicit, but it needs to be revised. + */ + function isFilterApplicable() { + const ip = getIndexPatternFromFilter(filter, indexPatterns); + if (ip) return true; - private togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, + const allFields = indexPatterns.map((indexPattern) => { + return indexPattern.fields.map((field) => field.name); }); - }; + const flatFields = allFields.reduce((acc: string[], it: string[]) => [...acc, ...it], []); + return flatFields.includes(filter.meta?.key || ''); + } - private onSubmit = (filter: Filter) => { - this.closePopover(); - this.props.onUpdate(filter); - }; + function getValueLabel(): LabelOptions { + const label = { + title: '', + message: '', + status: FILTER_ITEM_OK, + }; + if (indexPatternExists === false) { + label.status = FILTER_ITEM_ERROR; + label.title = props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorText', + defaultMessage: `Error`, + }); + label.message = props.intl.formatMessage( + { + id: 'data.filter.filterBar.labelErrorInfo', + defaultMessage: 'Index pattern {indexPattern} not found', + }, + { + indexPattern: filter.meta.index, + } + ); + } else if (isFilterApplicable()) { + try { + label.title = getDisplayValueFromFilter(filter, indexPatterns); + } catch (e) { + label.status = FILTER_ITEM_ERROR; + label.title = props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorText', + defaultMessage: `Error`, + }); + label.message = e.message; + } + } else { + label.status = FILTER_ITEM_WARNING; + label.title = props.intl.formatMessage({ + id: 'data.filter.filterBar.labelWarningText', + defaultMessage: `Warning`, + }); + label.message = props.intl.formatMessage( + { + id: 'data.filter.filterBar.labelWarningInfo', + defaultMessage: 'Field {fieldName} does not exist in current view', + }, + { + fieldName: filter.meta.key, + } + ); + } - private onTogglePinned = () => { - const filter = toggleFilterPinned(this.props.filter); - this.props.onUpdate(filter); - }; + return label; + } - private onToggleNegated = () => { - const filter = toggleFilterNegated(this.props.filter); - this.props.onUpdate(filter); - }; + // Don't render until we know if the index pattern is valid + if (indexPatternExists === undefined) return null; + const valueLabelConfig = getValueLabel(); - private onToggleDisabled = () => { - const filter = toggleFilterDisabled(this.props.filter); - this.props.onUpdate(filter); - }; -} + // Disable errored filters and re-render + if (valueLabelConfig.status === FILTER_ITEM_ERROR && !filter.meta.disabled) { + filter.meta.disabled = true; + props.onUpdate(filter); + return null; + } -export const FilterItem = injectI18n(FilterItemUI); + const badge = ( + props.onRemove()} + onClick={handleBadgeClick} + data-test-subj={getDataTestSubj(valueLabelConfig)} + /> + ); + + return ( + { + setIsPopoverOpen(false); + }} + button={badge} + anchorPosition="downLeft" + withTitle={true} + panelPaddingSize="none" + > + + + ); +} diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 6ff261e3cfb8a..f9328875cc910 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -26,6 +26,7 @@ import { Filter, isFilterPinned } from '../../../../common'; interface Props { filter: Filter; valueLabel: string; + errorMessage?: string; [propName: string]: any; } @@ -34,14 +35,17 @@ export const FilterView: FC = ({ iconOnClick, onClick, valueLabel, + errorMessage, ...rest }: Props) => { const [ref, innerText] = useInnerText(); - let title = i18n.translate('data.filter.filterBar.moreFilterActionsMessage', { - defaultMessage: 'Filter: {innerText}. Select for more filter actions.', - values: { innerText }, - }); + let title = + errorMessage || + i18n.translate('data.filter.filterBar.moreFilterActionsMessage', { + defaultMessage: 'Filter: {innerText}. Select for more filter actions.', + values: { innerText }, + }); if (isFilterPinned(filter)) { title = `${i18n.translate('data.filter.filterBar.pinnedFilterPrefix', { diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 417b6fb066172..6bc34a8b998a4 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -186,5 +186,37 @@ export default function ({ getService, getPageObjects }) { expect(filterCount).to.equal(1); }); }); + + describe('bad filters are loaded properly', function () { + before(async () => { + await filterBar.ensureFieldEditorModalIsClosed(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('dashboard with bad filters'); + }); + + it('filter with non-existent index pattern renders in error mode', async function () { + const hasBadFieldFilter = await filterBar.hasFilter('name', 'error', false); + expect(hasBadFieldFilter).to.be(true); + }); + + it('filter with non-existent field renders in error mode', async function () { + const hasBadFieldFilter = await filterBar.hasFilter('baad-field', 'error', false); + expect(hasBadFieldFilter).to.be(true); + }); + + it('filter from unrelated index pattern is still applicable if field name is found', async function () { + const hasUnrelatedIndexPatternFilterPhrase = await filterBar.hasFilter( + '@timestamp', + '123', + true + ); + expect(hasUnrelatedIndexPatternFilterPhrase).to.be(true); + }); + + it('filter from unrelated index pattern is rendred as a warning if field name is not found', async function () { + const hasWarningFieldFilter = await filterBar.hasFilter('extension', 'warn', true); + expect(hasWarningFieldFilter).to.be(true); + }); + }); }); } diff --git a/test/functional/fixtures/es_archiver/README.md b/test/functional/fixtures/es_archiver/README.md new file mode 100644 index 0000000000000..ca0f612fad06b --- /dev/null +++ b/test/functional/fixtures/es_archiver/README.md @@ -0,0 +1,10 @@ +## Changing test data sets + +If you need to update these datasets use: + + * Run the dev server `node scripts/functional_tests_server.js` + * When it starts, use `es_archiver` to load the dataset you want to change + * Make the changes you want + * Export the data by running `node scripts/es_archiver.js save data .kibana` + * Move the mapping and data files to the correct location and commit your changes + diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz index e83e34a2e07fab31fd8be22de647fdcd646dba12..a052aad9450f564426820c0354e43489a3e7e0c5 100644 GIT binary patch literal 20510 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ßgoDwAB@36~bC6353>h5$CVMgPw@{Q5YWJnSQ)Do=-_^`)3L8?$|wh6VTw zy7lrfFQ1X@sj{Ln5z`{2rbk4rKVhJX($yir{J@KUBgbyV&)^;b@Drb|0nTgjCJ@C# zR=BOp^+?1hPq^~nH(l0%iSw6cZq){24JmA-$_r5ZI?_a1N1R{5RnXxy98eSBldlY{ zi0q-3#>m|Hwba}Zt@x(v&+}{YV(~Q~M6M;Q;c``Q_?r1x#hUGi9C%V@1Ae> za~=1VO(aj;1b4&i2+Ol$U6+b)s;&&{ir(CnMW%>e$K^nl^$#?IY=NWiDyzO(JjxF8 z5)-|&ATTH}=Nvk{QvvhxO$IzLn9*T#KemLj4~t)Bb$`EdxuW)Y5U?C%JnvlSyLn9; zsN40X9~VB<28O?_P14Im>P&p!6YKEr%lJvO#oHn<_l;Fw-C1(WL{MLmWKsj$2;g|g zVS2wp#)&swyqMR%HX8pm2DufYv3X_%IxsTCd-LCxQJ$>0RC{j*ozjLr*Ix4;4gxb$ zB2OHTqs$Ws!I(L?o!@4v+H!^9BSC~&a`L|DtwF++;w<{!VLd{m%dl~Nu&SHsJQ)HpZ3QY>l~G`jEVq=N z6HnX#&c$<`V$noU<;{&qE{sZr@i`=;ksr&q^EPeHYvn;tnGEC;F&uHLZw?Ai3G-@^hh3wv8cz#0i7GcL zr!O@^X;eR4v>K}dm}Yr9Jy-%%(n>b3o3>?>*uv84aI3iX=HhnQDG>_E zvv>N~*ax7x+0_I%O(lu{>$A=yRaP7=CcWR=WMDuwjpV7I?lDMs3->$5XpfEFZyExfM=E%cW^ z96t3numu+tB188ew;8Hcc(70b%w|ySjYN*196fe{>P3&3XV4I4R->uxtk`2l0lUEp z&XL$r6BiSvzM4L&U{ajwx^1VqD3iSsJ(4C} zj~{g< z`f~3R@Xw$ERa4>&UvK9?#>ofxGsdi`4xHM8GGO7?P-u4SRX>dvT#aGuQ5jDE@E@AyEr z5|<>BVH#_ShW}Z(p>eYI${$B%b6LOglD%{NIiQF=2N`~*~&6I*2D~XWoN!Cmo4E4?$WBT zbP3#-HZSsMX5GL4>Z}pDBY_@Z9GhFcsIy)FOP`Yw){;3};%C^#WK*3_n|ni?G4Ihn z>2@Bw%Ijt>hIc-bt<+PYZRPME993378RC{$<7O%S2Pd+|$Zx=180~V@#;w-a26-{YHmuy4AuHik?ww@9weH zg|Fr5d)*PH@kko>yc@)X=y`?0m6Y3QQJk(vQ z_EGuu;`mpGbGI9neefMlL(*G?!7hC}j;4${H&bA#j@DDu)lCJ#m`xUr=a4HyQervSH(~p!3ii!R$-%pG+$1$DK5DMNg-WeO zbGH%iC01+cveX)c*`dy*3`;rfH}uMm3lGk+2{*4;>wT=&_K<0wgRjy0QhjAB_ditDsL@7xi>>{`Z==Zjo@ctuUy<%oM< zG7FTGFey+!AwbA$MJBF!1BxPMmKBXy$a2=_cO06w6Gg=WE(S)ie z@}RVBEDz!FV{bCNxC7^T=dx1>+?d_J)Y*x3RxbQqJFSh*uYyRFdTw${q~OvZT3 z#sdq1%2i_4=mouj*XR5Az)MR&?2q1b$8VdFf+HXCjE~-=;Y**S>RwTdx+q6*AJ|6k z_2QJi_oEE0!oA3?0NV7|fW({sos(buWPpzPwfw!?QMwo#RXl zpVb)5X%u)<)xFI4KIm1@%ADP7s-)Vitnzb=z*JQ?qur9!)X*T;_!P^OnPGvXSd5iu zl`%&LUwJe1(Pqj=Jz8U>d0*^$VT{O_eur8;cCtZTzMzDAF{JVWBioM=q1Ek!QY{)# z-yW^$$yv79&wi9h zYocmkX)>|PkM~DfVQ!bMc|OXuTUc^yLonz;k zC{{*Oi=o%-g%ynM9JU3y^#}=NZp#&#T%qB{SKlw-wx5djJK~YrKc;<=Zj~1Bl?7N! z?mQ;h69Rexfv+gM$(-`E_)fIjL<4*M{Leip~MHvjvTp9QW)@XifMV5 z_>qN$!T~MCJ8?^gjwh4hCV@C0mRT745eca5Q$u02h-05mW3|U{#@Wfx#u!E1ilXGp zcBEn`+z=Kn$IE=j~Z@espUr3BBNceqscDOVFfHZdN z?xbPr(ug^b+jIae2!h^W@TA?RLWVJTm$wgZ~!aFTp1jzGvJv zy>26?6>GPdeqmu2AvReIG|k-&f0gplgtb|ZWEI4jG;elLV9md8<<@4%8lukNfftY$K)L7y7_vs3!^WAwCrheq z(30OnbSGpl{UTP1mn8Zv`N{S{7c&RM`x`Cz;EaeoOj@28!HJW#qK=us*W(TQp`n1c z%=?9W$OyRWm|M;3jcbs_#}wCAQCYK*?8>oPceEjACR|>tGwoDRthI*gVhv=x!O!#@ zw1ykQxSb!^71Okh$h)nfvLmBX8G6nrcZOl+hU>&v=}5yWt;DROavM3l`K(!9`UT~4 zAUY}17mjgQbb)A7eP%R`r!I8-$J5x{BeM4L^`LiQu8VqGlGcp5BiMpwZVEJz%#5e0 zH@5?t?X<`(V#Q%Q9A{8Cc4Wg|Kl4ODebY6RXqZQCnwMndJ8;VTVfHAG_w(BB_uB2~ z_dDWkI3K`gIIyPEYM}Ky07MIs3I$ zgQDB8nEmwJVbZc_N13(faY5}+2735nB${DH8xg*$A@EA1+kwj7AIWATrV0jD16E@k zQ=U;q7Mn9lGt%$obY(u)q)hHLV|?;8W<}>pnHlL13x)~@Z&H<(fKpuXB+d(SL+t;0 zxx@{aR7lDreYa>>$(oTJl$VE*q&aG)CFG>@5k#mZ-s(|T1`q=u6kA(Dlhm1D^PzRDkAUV-ou(oJTDjYxY9hQRCK_6?$ zZv64dK@g2WY7yI=CwpLY74f7t@e)%eB99ji$Q|J?QNV93Zr&%XQ$9Jk;{4Pc_cn*q zFWJ!d%ru4j5?=ZUuU9BE9xi4o4_1!HlhVPwKk4~>#!@pl(Ivg~1!umf&J?JA>6VOz zoc7A7aaPxbWXa=$w-C64zUoAK~Zn{ClSUVmuj&lcFNT?Wy;-B6jWV>Z|I zF`aAYGFGno60O!HS&DGomCImRlOou#N+W4y+O8L@bszS1bGR(`*kc@oEU#%%Cseh( zHSFh$fTsEE<0to0F|QgdBa^Y!68PkOBhIDe^YElGj)wD!Yd7@SbTYP{*YLTGb(Vqx zvGy6RnDLF0WOz0cDpISend_8k&1nFZ^l~nYo)*$7Tf|$#>Xr0yFe`VQrjh08?QobO zX>nE1l7J=TJkVV4TdnsEydyd+jS4~~Qg2ZXTYxr2^j)Mk$ngYb^6#6l0Q|_b9C9cO z*a+Ap1{@CAj8#bI9KvYlwjl>%Fsg%y`JTX>{rnU=5 z&DmRQ7R+*P_5!TxQ((wlQ7m@HS!Xr%7B2 zFDWh$l#5LijE`Kwq}<(2 z%5m%^{0VV^DNV1^6MKp99qcI=(~}+-DPxd+SB)NCVu=OKJ4Fka!Nl@7eE|gB;Us3=VB)^!m%sCV? zMV);4GDKEG?oR!~>mjD|EB}ZXF}OgN11q`R3$|dsA$AhV7EwaM4~tX!Ao~4 ziEPa%jH~QHvr1&iCnv;m`T}oILdF4Ii z>Mpz%S6+fPAfbIyzf&AG=ta&n{ZS=Ss@0zx?2}=YJ1wt1Z%sEbi{e^uXO2xoU$mN; z->rYjY`7~MgI`>LLIwJqirdb&+^qz$4s=B=+wQ9TL({Nef>R?MkonsVdg&$FXU?C0 zlWI2zz!BxSb4d=R=%?@i#eEB!)i_6G3;Oj@))+jDA&J5sI3mqxFDu7@h2=OjEshqdAamI#}QA-hqQ+dVXsGVrm z@!!v{qX^?$EO6jc>c8PmsT$IzS0Mb-wF37(*!lu$9 zjDQW~Iu4q{Vg#}X0a}okWR+>$dC{gL!Pkx+^W>i!s~HMH5tHXUNio%9;eZ zoPLmNXkTp;)P#szm4j!lme}}&agSwIQ{mB%_UdffPG42qcPdQS=BOq zCc*11Ae9nnr*0*6JeMpzeLLfJ*Vq7FgUnKjGI_fT)!F|%(FPtYi9DD~f|+C8K z)aJU?XUC%GbizX1xWKdSFV6|uTf;JGbxkDLI3XzC5Y4qZC88h1<_Psg3U1|u)S|aM zC6yMfti&!$D75hnth77oaLo<8#QF&4EaMm7yr8fi5w|*}B*&VA`<%LXZ`5dg6wnr(SR3q_8LU9)hv^};b=620CE zSplzM#a-DzOR4c4Fehqic|^uBRb0)%K~K7d8!%Pg;DDfIsStCEAl?B?smTraoA7tT z1B`5ov(V-hhSg|khROVIw=+O=s|Q$SXTti`w`=@W$5Zp8U}m4Fs7ng&%EA-!H7{=- z<#zg>=C;Wsde1@zKAuBfV+d84`jHOy+66CF*fNUxv@5p=aEn#>3%!xM%B@$a?Rrr} z5jHE23KX>%V!0jAtP$>~Y;hmuj4+$X_sR69Vw>uU`S!wu@5;BNm9z%4xZ%8W!{YNg zJQXJMFk`uQyA(}KcUmh=OI`kay3)Z`F*SjnpNKN(Pqk@!H?p1WG|dmZx1BT^FHxZc z?fjQj50~%r(3?;xdfVk+Q9(bUK!=w*1P$-@K~C+`k~TFZ9VXvyN;~MiI@0r!sb0+_ zK<_pb{*JPOHXf2I;tF4ObHa)nU&XKX-#OCJTLcRG<(Oofr}cd{Mc=TD=5wje_(tY3 zS`1xQ|9KB3*)STLAPV>#YjyIYT*f!hSLzXPoUz?f4;HvZq=_5QgG7|(B zo1lf>66Z|kaA^qHSMy+x1&SQHN(2vWiCnzg$JC>LBhF0ffthJLD}P?L#GjpAM~Oe= zvoWILc@f|cj5mI)?p)Br$54IkGdV;8P4YY#bW-2~3weKPHU{7iUoq7c2s|Bnd)|4^ zAK*CmE;M!m?se&Iu07`nK_++T&YSqdbb%$7iYyT^+!mb*kU8fVK^L=^A4y-F&6I72 zdUp04?3!uUUtxI=-?v(!iLju>mV^mHT7@tOmA)kv%lkKFwx#*(=_V+I9DmXh;OdI` z3`8M+L$tYODVI!k<;mdTpa^`jiuPTeJ^GtosLgMORm0XZw60O>frF34nQw-!qenA^ zbVKjJk&0U|YwkF1zXPUQB%4^d6VbaoA7H>_AXP2RsO66i&F|H zde`jCd1r0Ok~3<7Dt*_y(w}ans1Pdv??28sv+4>4<%37b_Pi*Sx}Un7m%Iub>Dy%% z2W3}{T08Gxf4b!4GN_hyG#vr(0dV8GSiATz0jqD695BIkND2K{9k5DQc}ewSbUc+2 zUW<$|$G`<=!}hw_`abx}H*MH@fR2F+$UXn$kdXtu^PjETuvGw=j8bfvAn!_i{=^+S zAd9<_VuF`@IZL7`q&rKBf(q*tDL3ZZ^2A)}#{KTyImbxmC@SGDNlh+Y&qdCC77M|^ z_Brb}t4=)mhH2UQ0{n4Um1$rpbO5$Rkl8n2y!3M@Co&y6QF4H*Z<*!j3uHE0e0m~S zhVO1rW!BwBJN^&*IM6N8o8Z#&81QgGW|e!b-I++NjnYZ9lX~La?s#!qBi$^ewX>VP znTN4qBo*?>#{z~O@k!1BoUe~*UQL1RVc;iIR&VWX6F;DHeXWcXTnI1!Yi(!k0l&q( z85l8EdZ5;TtHc^hH?GRoyKDG+llk2yfl}rYYm@9T9y0xvaT#RycA~MxK=m^7K_C$i zAED3IP~+-CWtTodm179t;q4I(T;^NpiV_+|#E~)=alt14qJHgpZ`-PN zfmd6fnObCYT>(ZNvJp8vuK*V~(`l*YO-)&3BxN1S!8rIGufVgKRWCF#y&Mg{6Wc!mm3@($+b`aLI_m;=)@nmD&ohxD&od}SbGN?8VXpLOE9Tw0vQIr> z&q}Z~!$XeBJ^dAkCNKh^+BerX)xYLjoGSP7MuYp2ZaMOJb!%Tpa+XzZ?B2R0;T_kfI#lEW_>Tb9lA4%oxmRxNfIA zz{5I|b_5-Lb#*l;zH}0UTL`O+Y0sF`zUUn^PgKVqb}5e3C!OR#O|Q2Y4_WGgFTC!@ zU+?=T&{yUsX=zi=7#s$*?pCl1{b7|?p6r~AUgue7{tkZ#_yGwoUhM#o_46}oc=h*- zOXXd$SkE{@o=?nSFTt$bt=|uI<3{s_0r&99dmsvS6$Y{0#ZN9Cul&RVYp30x8L$^q zwCz^Jf)AL9-xKc4FoC)-d2tw$@N@vDveagiGMsZN()m*F_%p2nzg{Y0#RQ8)Z{A{R zjc?YDwjf4o++GhA5J;g)9V@FaUl9p%QGOi@2wqoDj95P%6?9RX%v7!CY10ZcBT?M$ z2QM_N#e!fLRKrd{{wp*b-95(51L2LNj5Rl`1nzU9zOL$P4cMf519-C)Mk!RdoW9ns zNnSiBRl5veIS<+>RRIl?wCGCT*Rt zaF(WeOgui6<51Wfrp-f}y0}H!2#YiyubpMKOx8V`2mSEKVoF!|j~x!8ck&GKeep%k z(4Atf?S*y>4rF5L!B1i$j;;kdUe=sRysr1#^UJ}Y*PELY7r>n617qSdl|j-<6Pxs( zl1|m3Ed|~Cg;Q84D3I;OUhR1z#Ta2|G*XJbc%a50@?72a`2*2%S!Dvgfk(A34syiW z#4U0ZA7KYar0-;ET)Ir91j&a=IcnbXa`CL9;9C>Rg1j=ctWq>VO<7b7ig8m~o0x}A9;`%2 zF(t0`x-V@QFe0SF1b0l2<7lzF_1|EP{$uUY59Ncne8<5DX~E_%7`>$FzaCFQD5hF; zd#(>;8J5wW)<~zMgfYS}A`0#6R^gMzo^$X46BM^aFk1hrL=C9NCoVn&$vrm>yk@3j zy=2}!oh7?KEWhcUGEi)pNcWbKtsRyDA&-QsJ~=zhio;O%DTU-jgU+d@F7!&48A*#I z#mxR>3#)qIGgAKe9bH;_dnp2+z;(DYLD`&H7NfOA>6$Xk6J)qj%K>Qjg?w*;mh0To zOP=MGA!yE4eH5DVN>KRyJWeD!jI}_O=KL%kl{T-`Z{y>k93GcaVo?=~R6AC%lu5c~ z7;5PIAKp$wRGLPLt*KWQV{L+^UHC=-5;-T!rLu(2U4q)>D{Urj zpIJr{)hI8ioEP-_)V!xjf%RkjY@08d%AZ&rAF}fS~4PG8>~cp4g)mB;P(% zeOKI1imFQsHQk_4HW{Vz(YQV_U;6%6B#Ni5ctb3)NtJtUIka{i)6By2$_<__YhPy9 zj%C&&oohzBh7uK5tCKa9GL1sP&p|gLA-|D+%mW+d6-QXX?tDM980QS>&XLZnlGphk zN*)yk&(h}G^-QpCc4liYivzo$cbmqA2cw%u zrPpmL4p8^Uvo4a4UmH{EO)`>1b?xJEGtBQsHA&&4CUjrpxHJ|`uvShMPl9Rvmu5gf z-(MHenleR_T84L%CQWs5GJp+;Y1JHwiSxS9`6*{>s3?s*bmqgNAO1f`qD7Dy>0G+&og8ZS9BaZEiGAmTeCt4UXtN4ARL4mI><`t$P|50MRErnUxs| ze|jB=2@E_gVEGp{DFEtSnkHng-f)dXGD>*$hj(-y%%%^hcE_o|t9TOf#61t_Miw^S z85-2>ZV^~bQ}1GTc`$ZIdneSQ|cS zEiGtUwjzejV|Mn674H;NcI3}3Fg!gMgg+KAFJg?-aI3Vqe$2;oa}g!hM)j(^bNd+h zm;E&UsBx91S4~Vl2yBS3B6%QA+81v8GGme!4t}mRC*tRCDo;?{66Fkv! zIBAbe=F`s9Ru=H6==KVWg>OcH7dU6qU+?%*i3s?Yid01O3@Hz=v{D^cI_WP zcjw)HyaRXr&g4vqP+5U!e*3!5p6hPg#mhoxk#pe>>B88G2 zj!%)T(jLdxnGj3k_Ea^Jd#~n=U}f3HsHiN^XjM?J%aa_j3j0%7EvMG{1_YJhokn+K zGHw4Sao@4QV#WQ*J_Y%y2Na9I`81NUXL*ho7dkg^s`=iMA zLa*Qac58oAw~B=qAtcRTaoC;3)(QwOSM*+nB0)s(m)b9ptgc#u?5gH!??@me1y)t7 zNypaf+4a$dw#S!PXBR$YxELnLx|Z$$3qKN^ezLMyFLh-`vO%mzegt)6M4K4#;)?G;LaP)^K^QJQ?HK%50*WD~jrV zm@FI~;F9#VvFB?c?Q4cmx7t!jxW(yT5Iu3$V;ZNKTm}r8me5X@xeCtX#oO(Z3!oh2 z>7`pf{RYV|rzgWOLd}01M}2gKB{{&Bo4Cexxb#U`6Xvth(b2a|anQ0~`{!vei*0`x z{SN=@I|I~~*(@FP4gD=#`79F#;yrgbdP`X9U?CpB;@cuTjUl2|$kwgyOAoRX3@M*8 zlza?{EF*Ye(iUJl=y60GjmKXVDBbzK$CEfRB9D~#{xr&e7zADDJ?RIUJ192(w7?tj zGq1lKG2J9_oDsq?2Ulk7Il6QJ_zoHA&SsYfsMe6Y?_dDx8o577B5tZ4Ncf$oCkMxC zl&;~9&rUHu&_Dffh=m|u4}c8-&pbpHp%0G!hr_^lBoOp|JDf~S_x(z9F7ad?(m5=+b0nM20$cs2K!7yHd99*ElzjVqd*OJDA}gy+f-; zF~*p4!;t`K_szLwBt25@Lwxv1e}g~T?izt0kSlVkC%0+5@DQX9t1alcZcv%r~52_`y;c`oy|(0rK2@fn(E)56p0Q z?NkaFTk#~7{=_kaaqMhIeC@i2 z-7428ua1KV2s5}ifzh^W?PF|xwTxgA5ODbTtJDR6>Z}r}P9PlULXweB5CgDGv3zRK z-7t|DAdt&26$0{~{vdrSXP_p6k%D-{fHy)}Q|yA`E3GkY{=oQXr5%tvr=H*A2>FkZQ&FTOt02;srYw7HUZrP#XvEAypGcX-m$? zRl>yj`M>7UqR~qZcWLlFV9(L#=OLw@?0S>#zDXy)P`8m(`vJ}lxb5~opH?-#2ulaB z%SDu{>~Q;z+X;Y|Y@xxin0=~6Cj{iLoys|e?z#HMEvLt`ECZKH=^sI(nVNtI6QLip z7ew_gQc=P<=FxsZSD4W5z4D&1hib;bnx1pN;qD|J>td6CJWh!{hbzvo*uNCI2khb} zNzH+z7{re{Nx<%HB|oR?Yneysad8(yVr4=@IG%AL0;-y!=p*XEJp5#jbx|Zk{RoiN zc!u50(3+S%n`tvNNMZCa1L5-O`OGv3CI!$Xeu*UJ2!$4b`duD$EI{>Y9R{cP@;^AT z@VB2re1TT37(s`cQcpQkArmGtkTTvO+t$j*@@Lm4P(%&4m$-+~@@MV_QRSK@qdV&X z5?$hp-(!}ln5-(w+Gg$j6A}!{K0O0CID?q z`->r^`(xn6>%qKWCCP=nV$T8hJIl8MErgDO{0a+zEXqFi__0L#}CcWP3wU_M0(`5J?#v-EQ$zZDn*96(kXH) zgfN({;vR)D0vGBiUCIQ$=tIB4{i~B~IE8HJ#a~3{%P#^x(T}=Ai>MTSD+mx3)~2XI z?=S$pmQ7Zkw%*S+mqXT_<>Y(09)t?7;LJf<2@OQzhU654oYd04rXFU??&!6jXA`ov zdJw4fN|wO%b`nZ!gDDsU{T}g;!NO5H3K}whX3*3p;};y? zT~U0?uO-;}#*Cb)E>FXD0F<5_b^x6I_jtUw^*>*BCGmg%mlu$$D+MuQKZr-C$H#|A zPfiyEo}kxUcYb1hFpN-- zq9VtGR@(ErHF+92nUf>-F>{o{MtpJyz1jHqIBL@Ja5P{YF^%5)o#0haCnnDEn_9cT zaovtgg8t)KxmGo{^UB{Yi+KT%`AQRYHee}59r9*I1>m>$xGC?lWbJlLu8VLbE8eI? z2C$)--9VlnmnkHBX;3)p)UN(j^zhQjxnX%8WlLS_SwxhAH5qbaHy9s-Q@2zQ=CCAE<$s92#n4k;z}x|KJpEQe1^$)IGUiTqH6vJ_T1H>u30Mqlif7w(H&ItpXU zkDn~<;6${s_4YY^kRNUMqTV>xcj8v5bet=>?p;$FhJIc9x9{U>R{6N!bD0g;H{a{;8@680@LY*;yv+#6S$^fLf{ z6vwN)CFc&Fo~}<@a^-Az^S-=zTiy?GyKFgv@+HieGsf5WTb^A{X}T1K?*o7jAdO?m0m!f@PMrZ7$XAP5PhAFA9 z_c>k5?$qtw7FTSLOZ-Jjc*Ri4>>7dGCOzJ?390@H&yMEk1fJflTpJ9ca~!eklJu&8 zx%408fz&KjldsF6kiIg%DLLy@Y=Fz@d;73?Wb(oWqO14UJa~&`nzY zEZH)yC@m(nlte?-?(o}fEIsa&Oolb1sH@@hV!QH!U8dGjG&WC_Xl$J@RVtV?bCi&u z2~dTaS)p`eZ8B6sohcNzY?wXKuq}fcblu!kuWfGoYk6TQqZ}(e$uzK;2ezYr(XIs6 zr5S4S`aU{wVY$dyImzbBN5{8lx~|<^o^~%Zuh}ma_3T zvd9Q(2nPx<42j*OT+9306*th8WlU1I`R=B{W5J7|e%Ng*RM{o(X)-hY0%`Ll?QT2@ z5z`e}C1=fjKb|a`!Q=4UW6Lzy$}F@fdZnA)SbM~MC9mKUrPQir6}w>o zw?&CX`_-%so94mgHC0wOF!QIQWcoi@9KylWtV42r=Aq1qcHZy*j^a?0{!bLA5lEjg zz47Uh?5L`4Y&orM9o)X?m*qV^1ro&f=h&e|{`asASiu&C*ZWFjadgiwVxT|5{qAUT z%LbV_AK;Ias#9GR#LYRSE|c;B9ZtN45J&zlImC1$VMq58_U*Is%%LWLC2w2rga8#h zya8c?-=N8Pw?CK~DKfOeP2HDeqVB|^-HN+=PGen6Lg=-r%d>QtobCLeGPRJ~8}r)Y zu|sI4(W}htleSBFW~D{mRy!u}@{Q!sKHSJP@#4`#1tG}O$oR?FbL{NJb@dKs)lR6; zG6N~)>-Kf++sz2SQnHtdo!GnzWZ*-j+gmm~s0xi-%Qfr#mFk3Sdn}|XurC=E#^a7` z^~XwpgKDsjN-&1<*U)qRF&tSWSYz3SsHk4c+6-kl2G*M*R#B`qq^hZvrTV3xxy4W-7voMi z7E6Q2Fujg2s{Tn94X=_$-YRdRpsI35e{iGSK}Wk~n>9(^da5|p3x%;)%g^q#ib=Z^ zy$X=(We-G^uA#!`cS6Ak@JhhL8g&V~5(tbslLqQe8zmLYMw;v(6Dr9q)_Z9&mNuu& z{tJ*Xyk)t_UN-pF9|yGx#p;9_t4-#O7fO{*5mZ)Y&AV5HI8poau@)4#kag6yK^$|< zv`ISFwa<%&{t~4!xihEgkb+A87mi+cV=W@3dFc{_y@&C?T*SZ0ayK-n@Ooa>41|)t zQ>>_|_Z(xGf4vt626>Cs`s2s#*J?09A|HH26>qqM`VKdO?V2z8kZ;KM2#hk!+#WK9 zZTE1!MjS4nWJxk=*-eHy?bnmKl{C7-Zi=pXz&+gIwi0If+fXd225yx&i*=v>iHvQt zlUoBM$xJ1`-!DrMB{Tu|_d9@!-b<~{-lbizN2apdl7@Sshf}p;I!DJ2$}Y0rHX893 zk2em5w`_9ncH#XrOypcjjrJvj+i4C1_v=U$JpV1zjH>a?PjaSk_**OvPHf8D1)kM% zs`obB^|Wn$u7G_(csc%;wG=wp(UC|6r%r^_q`LMABBNVfhbB9H3r6ZL3d!Uck$stI zj2C95ON&s7*bw<+E7fC`cdr!+45{{;fUW^yMWkKT> z!E9_O+gfG(8e-Ap>{!j6AuRmj{?)YuPVt*>s=BC5RY5oG1y)Kt5TsAe7bR7?zZ#D4 zGK(l4Aa0Eo5*PoRyquxhZH6MFZ(4)({&^=3ETPW7mW-n$hHlxxaB3>rlZc`U*O3a% zV3{s?ptty;3e=>@-8_UTNCLawvyF=#u#~7dqEaEVQv>vwrtrT3TqJo}Et$)eCNIgk zyquw+o58*+RcOnl*w-re?IkT6^81}499H``rmCb>ldg8h)whbBA0f;PRjiMLmiLy( zW<|q3?NhlhR&k*vX5Ay_3Rb$SO*pG5-L^_6A7+@KYGAH)+W4RE*?lGHhp(0?jZ832w^4u9$a0JDj7a7e&xQ_5r7jcC zJ7;L+a4B>S@q+e|AoZ2fNOGAPn=vE`-0{Ye z2-%V_|4*rZ7N)023hVLCi@r@zi7jI3*L4cPl=k8Tr@%J;=vfmpxf9=4tv$>#BD_fV zKj+d?aog0r2Aug29dF$eZyZVX2=h;yFF$>{lA zz(IeNkcj`)vfH6II@D=ezJY`H;zOaf=dGy-!LeB(U}CSR-WeKO-Wl6ZcXy9XQGh|qESu5HBf77NyYA%>+~VZ&a0+dA=wZBJv;Y%x_nCXvO+2*L6Yj)rvu>quNIqw#eBNNv@GJBsOI7SY166 z#$rXWp?nv_%sWYnjOMb^v&qd~GupF(< zFLAc_EMlYwY5B@z!*q}|=g?MA^*>fWoCs!sl#CR_flv%%ZA>)kDJ~BC-?g+W@gZU{ z^68>f!ClzM%JgatLfb6w!Xfo$Kp{B9Z?}KRX{=r|e@y7qH^M#C&oq z?Zt=T;Ia2tdvfdB1PmmZ;j)rtn#j40GgrB-b`EoyWVdJ{bAI%*EQBVMFNGHEzh#EfJj)n##(*y{=ddenCaBefuTguNdrBrPjYdrAPC!_;35f8^_;P z3bO@o#Mm<_M@SO;Xr+UAR;09G8lA57M~MlBRJoVV$H}Pt4$Lb{0X}steK-1fwfa3S zf=0pH|KyNwStFl)|Ip7B{LyrlJSE6O0K`J{JLX-w>G%Od|JB8jWh5WO#qa+Kbw{VK zj_Br~D`3PJ{~URl2mCkG?F)ZfM4&T5(ivotIZyee>Ib#qKEpBdFph{!kau5)1=v~Y zH6(sS%})V%;=QdBD@=d05Jd9pf$)dGDB6?zp!-S|*qs%6Cw$=x>L<;w9S3<*^xctW zCRk<4bQBNIi}$x1Izz1YF93+j>jnBpbtfd^{6RxPCGqcJ@z%I}BcOx3B?O=bfI?fz z`X6@uv$9DIuzjKahv@+&1_&3L21@ z;Fq}iBi+~wIKB;B$-X0XrZ0|m$jG-1o)@#LeF4l*j_{rVJ&WOeP$gE9aHKt0W@UaD z^gUJPOCBLDu2~=82c=Fx`0X~B`jB7SzQP?VG5iw6oKXVvIevWaGTDq;&K@COB$;Fj zJdkjBSdkt{=va*0UP}C5Y@DOy1n=0N8+I6qhV{lv8pdu#Z=emDu0D|aDPr4RbfUOS zESrQIG6=g-cwTmad|=u70bl!s!%l^3cpH@-B-klz4S@7KG$yK@%r%8UJkaEIsNFO` zMSmS6oA1AoE zq%B2AV?5}Oc)cQ{6CrgARnVmh*eGA0mmE4YM#;f;HU2xSImVnEqT>O`99;#2(NE`nJ#iW*z}{DZQHCW?YkH~E{@c4$ zZpMy)LcUpV7V-nY|4*vhZ=w1O zx1XakF>^87Zb-T8^nU`~6p8-%z+RVoZHtiI8ixT%zCRu!2QY5z0loaop4RMpyH2SezEEhhGQRAF7%B*0VsL<53;T_$49M@ zH)&6}LLoqVfix(%2OGN7CK{dKKOoZVYH|ic??xpxniQY^o8`*ZGrMIwJ_iu^|KBW^ z4;c>IbhbZE1qO0O(&i_)$L^;%K^n+X8I@kKa17)<8@3eKcmD7+gB!62gp-ml$TFhu z=k5Oub1NLB`_jo<&@1Os>Lz-Dhw9MIf~nE7*BGSG_KFyjEp3s%=U`Lu+<2uH0O|t3 z)043V-aRqHtCKdRQB`foM`=yZ8E3GOcVhE#WWz%$DW5Z&B%7K~*?h+X$emWWT%Gi+R_gTy57LXamPWs2H$VMDx!!2?N~<3$wXj7xtM7I} z0;N2=4|#7#stAuK z4dpWwj_8pHcv%M&6i8esV+?7M_8=B8sgd}l3rPnPPJcW>q`doorM%<6jK;UI7<~5d zV9L$Sv17im;JfYrIs`ffd^d_ z13`v>Dx!m>uv6R_@c2wO+Yw=)*+Tq+14vB5P}s#U@El=!=Kp`7T!#xvuJ+QmaKfe?tf}C-xPD<;W<=OgnjX~6MyAf*Y5m_p&eM>w)gd<{O4Fj(4!P&U7 z!s&O|p28vGAgLxw$e*T>rg=(b!wRUg$@*%cGQ(|B+Q0IUvI=7aLzfM%U|b^%`%S)N zK9+XDE)gqVwaXYiE{*|QhCITLZ&egv>r8Tg5I^zNg=sKB; zq9=M%9-(Y>%$gR97Af;l$bJ$hj|Z*n+@7L;KfW^msS7`nhy0Ap|HpuN$VI&x_U*s1 z4*jvLCkSc8(FNrDpQ6xl{fULyy!g5vLN&b|Q~bV|pH=US++L9A_sPRbyZg6m{$TRG z!BJgi9~e&^YF5&N{vreZ6q)c3h!hi-KOpUL3v zCtg=%SdDuunNf{mJ<@l7b}GmpNk1rBdiv}hMiMX`qDI=@BZkwhYl{+3%_Fv@%I9d_ z-gSSDmTTTuJp03JOxqJ1=iVQ)kFC6lC~d>`da`xx@b1Y$Cl;W_Fx@fF*4h&TGKvU~ z>kGzJ`lf9Bk2h6Xy`0*oi@5>*t6|k73}|&Zjx4sPzyIBVu_EWKF7J-TI7 zY?e!(DsvqZNCw6w#!gjsPsVGX!=a$(XKq>Ot)}K)DJ(`(-1Kf5u-X*%y0CjB@l_5- zVFSjfy(Q2U)sng67E~t}L~A5b@|$_2&neU~k}lc#I*vXiBx%HJXoAHz!bV0>ZHOFE z7-f=f^{cK`s&FP~+U6sS8dA23m8CQ4=Lzc*yc!%!3Ov!>Cxljh|8<2aZa>S1o+_qh zQ|GKt$l+QY<65a&ly>iR^wefu_EwFmG-ossKijq<1l*qTi?xAL>8Him813 zl7V`-ao&K;H#|uNPRcrYJ{o_4HAQBrfY0aynHr+8C zi%$Qb$E|$CsDM}H+?vP28s%4w{2bfUY%A+L&fV|nOzb;CshnbJFIBcVir&f!Y^?w$ z8KDf-grve_?{nBQo2fXVDdbq-=zwFhR-(_ktyk(^I)ssOsj#V88|)%n@hRtQX*cv$ zuVY}G-06>lkIXUXOf}YL)DX?i|BNkZz|dH0799$KY3OTepm1JTPHyguHjt`RPuk#B zFX-bN@(e38n-(mymxH*u@*L~*_<6rBqNx&{hD9Z+9geJHGA{Ew^V~&CmtpcCW~e%F z$OaeExE;_>x_n-kmp{X!D?Psz90sJp%Vv-~P diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json b/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json index 7f4cdd2906d44..9f5edaad0fe76 100644 --- a/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json +++ b/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json @@ -1,13 +1,74 @@ { "type": "index", "value": { - "index": ".kibana", + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, "dynamic": "strict", "properties": { + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, "config": { "dynamic": "true", "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, "buildNum": { "type": "keyword" }, @@ -40,6 +101,9 @@ }, "notifications:lifetime:warning": { "type": "long" + }, + "xPackMonitoring:showBanner": { + "type": "boolean" } } }, @@ -92,9 +156,6 @@ "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } @@ -122,6 +183,122 @@ }, "title": { "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "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" + }, + "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" } } }, @@ -161,6 +338,34 @@ } } }, + "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": { @@ -202,9 +407,23 @@ } } }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, "type": { "type": "keyword" }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, "updated_at": { "type": "date" }, @@ -222,7 +441,6 @@ "url": { "fields": { "keyword": { - "ignore_above": 2048, "type": "keyword" } }, @@ -242,7 +460,7 @@ } } }, - "savedSearchId": { + "savedSearchRefName": { "type": "keyword" }, "title": { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dbb2a85eb2a22..bbb622f452967 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -565,7 +565,6 @@ "data.filter.filterBar.filterItemBadgeIconAriaLabel": "削除", "data.filter.filterBar.includeFilterButtonLabel": "結果を含める", "data.filter.filterBar.indexPatternSelectPlaceholder": "インデックスパターンの選択", - "data.filter.filterBar.labelErrorMessage": "フィルターを表示できませんでした", "data.filter.filterBar.labelErrorText": "エラー", "data.filter.filterBar.moreFilterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", "data.filter.filterBar.negatedFilterPrefix": "NOT ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5527385c752e3..07169a2b73da8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -565,7 +565,6 @@ "data.filter.filterBar.filterItemBadgeIconAriaLabel": "删除", "data.filter.filterBar.includeFilterButtonLabel": "包括结果", "data.filter.filterBar.indexPatternSelectPlaceholder": "选择索引模式", - "data.filter.filterBar.labelErrorMessage": "无法显示筛选", "data.filter.filterBar.labelErrorText": "错误", "data.filter.filterBar.moreFilterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", "data.filter.filterBar.negatedFilterPrefix": "非 ", diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index 9b5b82efe6f51..a996910d4787a 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -32,8 +32,9 @@ export default function ({ getPageObjects, getService }) { await testSubjects.click('mapTooltipCreateFilterButton'); await testSubjects.click('applyFiltersPopoverButton'); - const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); - expect(hasSourceFilter).to.be(true); + // TODO: Fix me #64861 + // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); + // expect(hasSourceFilter).to.be(true); const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); expect(hasJoinFilter).to.be(true); From 4a1b05c843c39beb3ea2d4db35f444e4b120c428 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 3 Jun 2020 11:35:55 +0200 Subject: [PATCH 67/69] Lens editor auto refresh (#65868) --- .../public/react_expression_renderer.test.tsx | 25 ++++++++++++++ .../public/react_expression_renderer.tsx | 16 ++++++++- .../editor_frame/editor_frame.tsx | 1 + .../editor_frame/suggestion_panel.test.tsx | 2 ++ .../editor_frame/suggestion_panel.tsx | 19 +++++++++-- .../editor_frame/workspace_panel.test.tsx | 34 ++++++++++++------- .../editor_frame/workspace_panel.tsx | 9 ++++- 7 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 702f88d785756..7c1711f056d69 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -88,6 +88,31 @@ describe('ExpressionRenderer', () => { expect(instance.find(EuiProgress)).toHaveLength(0); }); + it('updates the expression loader when refresh subject emits', () => { + const refreshSubject = new Subject(); + const loaderUpdate = jest.fn(); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$: new Subject(), + data$: new Subject(), + loading$: new Subject(), + update: loaderUpdate, + destroy: jest.fn(), + }; + }); + + const instance = mount(); + + act(() => { + refreshSubject.next(); + }); + + expect(loaderUpdate).toHaveBeenCalled(); + + instance.unmount(); + }); + it('should display a custom error message if the user provides one and then remove it after successful render', () => { const dataSubject = new Subject(); const data$ = dataSubject.asObservable().pipe(share()); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index a83c63443906b..bf716a3b9b1e8 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -19,7 +19,7 @@ import React, { useRef, useEffect, useState, useLayoutEffect } from 'react'; import classNames from 'classnames'; -import { Subscription } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; @@ -38,6 +38,10 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; onEvent?: (event: ExpressionRendererEvent) => void; + /** + * An observable which can be used to re-run the expression without destroying the component + */ + reload$?: Observable; } export type ReactExpressionRendererType = React.ComponentType; @@ -63,6 +67,7 @@ export const ReactExpressionRenderer = ({ renderError, expression, onEvent, + reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); @@ -135,6 +140,15 @@ export const ReactExpressionRenderer = ({ }; }, [hasCustomRenderErrorHandler, onEvent]); + useEffect(() => { + const subscription = reload$?.subscribe(() => { + if (expressionLoaderRef.current) { + expressionLoaderRef.current.update(expression, expressionLoaderOptions); + } + }); + return () => subscription?.unsubscribe(); + }, [reload$, expression, ...Object.values(expressionLoaderOptions)]); + // Re-fetch data automatically when the inputs change useShallowCompareEffect( () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 90405b98afe65..07c76a81ed62d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -305,6 +305,7 @@ export function EditorFrame(props: EditorFrameProps) { dispatch={dispatch} ExpressionRenderer={props.ExpressionRenderer} stagedPreview={state.stagedPreview} + plugins={props.plugins} /> ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 6b0f0338d4015..fd509c0046e13 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -21,6 +21,7 @@ import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import chartTableSVG from '../../..assets/chart_datatable.svg'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; jest.mock('./suggestion_helpers'); @@ -85,6 +86,7 @@ describe('suggestion_panel', () => { dispatch: dispatchMock, ExpressionRenderer: expressionRendererMock, frame: createMockFramePublicAPI(), + plugins: { data: dataPluginMock.createStartContract() }, }; }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 0f0885d696ba4..b06b316ec79aa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -24,10 +24,14 @@ import classNames from 'classnames'; import { Action, PreviewState } from './state_management'; import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; -import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { + ReactExpressionRendererProps, + ReactExpressionRendererType, +} from '../../../../../../src/plugins/expressions/public'; import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; const MAX_SUGGESTIONS_DISPLAYED = 5; @@ -52,6 +56,7 @@ export interface SuggestionPanelProps { ExpressionRenderer: ReactExpressionRendererType; frame: FramePublicAPI; stagedPreview?: PreviewState; + plugins: { data: DataPublicPluginStart }; } const PreviewRenderer = ({ @@ -154,6 +159,7 @@ export function SuggestionPanel({ frame, ExpressionRenderer: ExpressionRendererComponent, stagedPreview, + plugins, }: SuggestionPanelProps) { const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates; const currentVisualizationState = stagedPreview @@ -204,6 +210,13 @@ export function SuggestionPanel({ visualizationMap, ]); + const AutoRefreshExpressionRenderer = useMemo(() => { + const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$(); + return (props: ReactExpressionRendererProps) => ( + + ); + }, [plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$, ExpressionRendererComponent]); + const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1); useEffect(() => { @@ -296,7 +309,7 @@ export function SuggestionPanel({ defaultMessage: 'Current', }), }} - ExpressionRenderer={ExpressionRendererComponent} + ExpressionRenderer={AutoRefreshExpressionRenderer} onSelect={rollbackToCurrentVisualization} selected={lastSelectedSuggestion === -1} showTitleAsLabel @@ -312,7 +325,7 @@ export function SuggestionPanel({ icon: suggestion.previewIcon, title: suggestion.title, }} - ExpressionRenderer={ExpressionRendererComponent} + ExpressionRenderer={AutoRefreshExpressionRenderer} key={index} onSelect={() => { trackUiEvent('suggestion_clicked'); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 59b5f358e190f..49d12e9f41440 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -21,11 +21,17 @@ import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { Ast } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; -import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { + DataPublicPluginStart, + esFilters, + IFieldType, + IIndexPattern, +} from '../../../../../../src/plugins/data/public'; import { TriggerId, UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; describe('workspace_panel', () => { let mockVisualization: jest.Mocked; @@ -34,6 +40,7 @@ describe('workspace_panel', () => { let expressionRendererMock: jest.Mock; let uiActionsMock: jest.Mocked; + let dataMock: jest.Mocked; let trigger: jest.Mocked>; let instance: ReactWrapper; @@ -41,6 +48,7 @@ describe('workspace_panel', () => { beforeEach(() => { trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked>; uiActionsMock = uiActionsPluginMock.createStartContract(); + dataMock = dataPluginMock.createStartContract(); uiActionsMock.getTrigger.mockReturnValue(trigger); mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); @@ -69,7 +77,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -92,7 +100,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -115,7 +123,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -152,7 +160,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -240,7 +248,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -292,7 +300,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -372,7 +380,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); }); @@ -427,7 +435,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); }); @@ -482,7 +490,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); @@ -520,7 +528,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); }); @@ -564,7 +572,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); }); @@ -620,7 +628,7 @@ describe('workspace_panel', () => { dispatch={mockDispatch} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock }} + plugins={{ uiActions: uiActionsMock, data: dataMock }} /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index 44dd9f8364870..76da38ead6523 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -36,6 +36,7 @@ import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -54,7 +55,7 @@ export interface WorkspacePanelProps { dispatch: (action: Action) => void; ExpressionRenderer: ReactExpressionRendererType; core: CoreStart | CoreSetup; - plugins: { uiActions?: UiActionsStart }; + plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; } export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel); @@ -135,6 +136,11 @@ export function InnerWorkspacePanel({ framePublicAPI.filters, ]); + const autoRefreshFetch$ = useMemo( + () => plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$(), + [plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$] + ); + useEffect(() => { // reset expression error if component attempts to run it again if (expression && localState.expressionBuildError) { @@ -224,6 +230,7 @@ export function InnerWorkspacePanel({ className="lnsExpressionRenderer__component" padding="m" expression={expression!} + reload$={autoRefreshFetch$} onEvent={(event: ExpressionRendererEvent) => { if (!plugins.uiActions) { // ui actions not available, not handling event... From 0a23bfb08cb695f93a62854806bea604bfe4e321 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 3 Jun 2020 13:29:54 +0200 Subject: [PATCH 68/69] TSVB: handle division by zero in math agg (#67111) --- .../response_processors/series/math.js | 36 +++-- .../response_processors/series/math.test.js | 148 ++++++++++++++++++ 2 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js index 6c8fe7ca16619..f8752ce8fa3a8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js @@ -91,21 +91,29 @@ export function mathAgg(resp, panel, series, meta) { // a safety check for the user const someNull = values(params).some((v) => v == null); if (someNull) return [ts, null]; - // calculate the result based on the user's script and return the value - const result = evaluate(mathMetric.script, { - params: { - ...params, - _index: index, - _timestamp: ts, - _all: all, - _interval: split.meta.bucketSize * 1000, - }, - }); - // if the result is an object (usually when the user is working with maps and functions) flatten the results and return the last value. - if (typeof result === 'object') { - return [ts, last(flatten(result.valueOf()))]; + try { + // calculate the result based on the user's script and return the value + const result = evaluate(mathMetric.script, { + params: { + ...params, + _index: index, + _timestamp: ts, + _all: all, + _interval: split.meta.bucketSize * 1000, + }, + }); + // if the result is an object (usually when the user is working with maps and functions) flatten the results and return the last value. + if (typeof result === 'object') { + return [ts, last(flatten(result.valueOf()))]; + } + return [ts, result]; + } catch (e) { + if (e.message === 'Cannot divide by 0') { + // Drop division by zero errors and treat as null value + return [ts, null]; + } + throw e; } - return [ts, result]; }); return { id: split.id, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js new file mode 100644 index 0000000000000..79cfd2ddd54bb --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mathAgg } from './math'; +import { stdMetric } from './std_metric'; + +describe('math(resp, panel, series)', () => { + let panel; + let series; + let resp; + beforeEach(() => { + panel = { + time_field: 'timestamp', + }; + series = { + chart_type: 'line', + stacked: false, + line_width: 1, + point_size: 1, + fill: 0, + id: 'test', + label: 'Math', + split_mode: 'terms', + split_color_mode: 'gradient', + color: '#F00', + metrics: [ + { + id: 'avgcpu', + type: 'avg', + field: 'cpu', + }, + { + id: 'mincpu', + type: 'min', + field: 'cpu', + }, + { + id: 'mathagg', + type: 'math', + script: 'divide(params.a, params.b)', + variables: [ + { name: 'a', field: 'avgcpu' }, + { name: 'b', field: 'mincpu' }, + ], + }, + ], + }; + resp = { + aggregations: { + test: { + meta: { + bucketSize: 5, + }, + buckets: [ + { + key: 'example-01', + timeseries: { + buckets: [ + { + key: 1, + avgcpu: { value: 0.25 }, + mincpu: { value: 0.125 }, + }, + { + key: 2, + avgcpu: { value: 0.25 }, + mincpu: { value: 0.25 }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + test('calls next when finished', () => { + const next = jest.fn(); + mathAgg(resp, panel, series)(next)([]); + expect(next.mock.calls.length).toEqual(1); + }); + + test('creates a series', () => { + const next = mathAgg(resp, panel, series)((results) => results); + const results = stdMetric(resp, panel, series)(next)([]); + expect(results).toHaveLength(1); + + expect(results[0]).toEqual({ + id: 'test:example-01', + label: 'example-01', + color: 'rgb(255, 0, 0)', + stack: false, + seriesId: 'test', + lines: { show: true, fill: 0, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { fill: 0, lineWidth: 1, show: false }, + data: [ + [1, 2], + [2, 1], + ], + }); + }); + + test('turns division by zero into null values', () => { + resp.aggregations.test.buckets[0].timeseries.buckets[0].mincpu = 0; + const next = mathAgg(resp, panel, series)((results) => results); + const results = stdMetric(resp, panel, series)(next)([]); + expect(results).toHaveLength(1); + + expect(results[0]).toEqual( + expect.objectContaining({ + data: [ + [1, null], + [2, 1], + ], + }) + ); + }); + + test('throws on actual tinymath expression errors', () => { + series.metrics[2].script = 'notExistingFn(params.a)'; + expect(() => + stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([]) + ).toThrow(); + + series.metrics[2].script = 'divide(params.a, params.b'; + expect(() => + stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([]) + ).toThrow(); + }); +}); From f8759d4571101d4410373b2fb01b9226f8ae66db Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 3 Jun 2020 13:34:14 +0200 Subject: [PATCH 69/69] Deprecate kibana.defaultAppId setting (#67635) --- docs/setup/docker.asciidoc | 2 +- docs/setup/settings.asciidoc | 4 +++- src/plugins/kibana_legacy/server/index.ts | 20 +++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 12ee96b21b0c7..e8029ed1bbe9b 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -93,7 +93,7 @@ Some example translations are shown here: [horizontal] **Environment Variable**:: **Kibana Setting** `SERVER_NAME`:: `server.name` -`KIBANA_DEFAULTAPPID`:: `kibana.defaultAppId` +`SERVER_BASEPATH`:: `server.basePath` `MONITORING_ENABLED`:: `monitoring.enabled` In general, any setting listed in <> can be diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 42d616c80119b..1be9d5b1ef35b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -216,7 +216,9 @@ on the {kib} index at startup. {kib} users still need to authenticate with | Enables use of interpreter in Visualize. *Default: `true`* | `kibana.defaultAppId:` - | The default application to load. *Default: `"home"`* + | *deprecated* This setting is deprecated and will get removed in Kibana 8.0. +Please use the `defaultRoute` advanced setting instead. +The default application to load. *Default: `"home"`* | `kibana.index:` | {kib} uses an index in {es} to store saved searches, visualizations, and diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 98c754795e947..0188f9b1ec515 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -17,7 +17,13 @@ * under the License. */ -import { CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; +import { + ConfigDeprecationLogger, + CoreSetup, + CoreStart, + PluginConfigDescriptor, +} from 'kibana/server'; +import { get } from 'lodash'; import { configSchema, ConfigSchema } from '../config'; @@ -29,6 +35,18 @@ export const config: PluginConfigDescriptor = { deprecations: ({ renameFromRoot }) => [ // TODO: Remove deprecation once defaultAppId is deleted renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), + (completeConfig: Record, rootPath: string, log: ConfigDeprecationLogger) => { + if ( + get(completeConfig, 'kibana.defaultAppId') === undefined && + get(completeConfig, 'kibana_legacy.defaultAppId') === undefined + ) { + return completeConfig; + } + log( + `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead` + ); + return completeConfig; + }, ], };